Documentation
¶
Overview ¶
Package geomap is a GoFastr plugin that ships a TRUSTED host-page interactive vector map built on MapLibre GL + OpenFreeMap tiles. Unlike the richtext, mermaid, and monaco plugins it does NOT run inside a sandboxed opaque-origin iframe: a vector map MUST fetch() tiles and spawn the MapLibre web worker, both impossible under the opaque frame's `connect-src 'none'`. It therefore runs in the host page's own origin with the host page's own CSP, exactly like the tour plugin.
OpenFreeMap (https://tiles.openfreemap.org) is MIT-licensed, free for commercial use, no API key, no rate limits, no cookies. Attribution (OSM + OpenMapTiles) is auto-added by MapLibre from the style — do not strip it.
The plugin serves two NON-framed host-page assets (map.js + map.css) plus a save endpoint. The runtime is injected into host pages through UIHostOption (a host app mounts a UIHost with it) or loaded by the self-contained demo page (see WithDemoPage). There is no platform broker, no adapter, no tile proxy — MapLibre fetches OpenFreeMap directly.
Index ¶
- Constants
- Variables
- func DefaultCapabilities() []string
- func UIHostOption() uihost.Option
- type GeocodeResult
- type Geocoder
- type MapConfig
- type MountConfig
- type Option
- func WithCapabilities(caps ...string) Option
- func WithCenter(lat, lng float64) Option
- func WithClusterMaxZoom(z float64) Option
- func WithClusterRadius(px float64) Option
- func WithClustering() Option
- func WithDemoPage() Option
- func WithDevGrantAll() Option
- func WithGeocodeEndpoint(endpoint string) Option
- func WithGeocodeUserAgent(ua string) Option
- func WithGeocoder(fn Geocoder) Option
- func WithMapConfig(cfg MapConfig) Option
- func WithMarkers(markers []mapMarker) Option
- func WithMaxZoom(z float64) Option
- func WithMinZoom(z float64) Option
- func WithReadOnly() Option
- func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option
- func WithSearch() Option
- func WithStyle(name string) Option
- func WithStyleBaseURL(url string) Option
- func WithStyles(names ...string) Option
- func WithTheme(theme string) Option
- func WithZoom(z float64) Option
- func WithoutGeolocateControl() Option
- func WithoutScaleControl() Option
- type Plugin
- func (p *Plugin) Capabilities() []string
- func (p *Plugin) DefaultConfig() MapConfig
- func (p *Plugin) Init(app *framework.App) error
- func (p *Plugin) LoadDoc(ctx context.Context, docID string) (docJSON string, ok bool)
- func (p *Plugin) Mount(cfg MountConfig) render.HTML
- func (p *Plugin) Name() string
- type SaveRequest
Constants ¶
const ( Name = "map" Version = "0.3.0" RoutePrefix = "/__gofastr/plugin/map" MapJSURL = RoutePrefix + "/map.js" MapCSSURL = RoutePrefix + "/map.css" SaveURL = RoutePrefix + "/save" // GeocodeURL is the SAME-ORIGIN place-search proxy, registered only when // [WithSearch] is set. The browser never calls a geocoder directly: routing // it through the plugin is what lets us set a policy-compliant User-Agent, // rate-limit, and cache — and keeps the host page CSP at connect-src 'self'. GeocodeURL = RoutePrefix + "/geocode" DemoURL = "/map" SchemaVersion = "map-v1" )
Identity and route constants. The Go package / directory is `geomap` (the identifier `map` is a Go keyword), but the user-facing identity strings are "map" (Name, route prefix, demo URL, schema). Both this file and js/src/map.ts hard-code these exactly — they ARE the contract. The demo lives at /map so it co-mounts with the other plugin demos without colliding on "/".
const CapGeocode = "geocode:search"
CapGeocode gates the place-search proxy. It is NOT in DefaultCapabilities: search is opt-in, so the capability is appended by New only when WithSearch is set. A host that overrides the grant set with WithCapabilities and still wants search gets it appended too — the gate is on egress the host explicitly enabled, so silently dropping it would just break search with a 403.
Variables ¶
var ErrConflict = errors.New("geomap: save conflict")
ErrConflict is the sentinel a WithSaveHandler hook returns to signal that the save lost an optimistic-concurrency check — the stored document changed under the map since it loaded. handleSave maps it to HTTP 409 (E_CONFLICT) rather than the generic 500 (E_SAVE), so the map can warn the user instead of silently dropping their pins. Wrap it (fmt.Errorf("...: %w", geomap.ErrConflict)) to add context; handleSave uses errors.Is. Identical to monaco's contract.
Functions ¶
func DefaultCapabilities ¶
func DefaultCapabilities() []string
DefaultCapabilities is the grant set advertised to the host. Geomap has no upload path — only document read/write + theme:read (same as monaco).
func UIHostOption ¶
UIHostOption returns the uihost.Option that injects the map runtime into every UIHost-rendered page. Apps using a UIHost pass this to uihost.New; the runtime then scans for [data-fui-geomap] mount elements and renders a MapLibre map into each, injecting MapLibre's own CSS. The overlay map.css is NOT injected here — a host wanting the demo-style overlay links MapCSSURL itself (see docs/geomap.md).
The platform broker is NOT needed (the map is a trusted host-page plugin, no sandboxed iframe), so only the runtime script is injected.
Types ¶
type GeocodeResult ¶
type GeocodeResult struct {
Label string `json:"label"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}
GeocodeResult is one place-search hit. The json tags are the wire contract map.js reads (label/lat/lng) — the search control renders `label` and flies to lat/lng.
type Geocoder ¶
type Geocoder func(ctx context.Context, query string) ([]GeocodeResult, error)
Geocoder resolves a free-text place query to candidate results. Return an empty slice (not an error) for "no matches" — an error means the lookup itself failed and is surfaced to the browser as a 502.
type MapConfig ¶
type MapConfig struct {
Center geoPoint `json:"center"`
Zoom float64 `json:"zoom"`
MinZoom float64 `json:"minZoom"`
MaxZoom float64 `json:"maxZoom"`
Style string `json:"style"` // name ("liberty") or full URL
StyleBaseURL string `json:"styleBaseURL"` // default https://tiles.openfreemap.org/styles/
Styles []string `json:"styles"` // switcher options
ReadOnly bool `json:"readOnly"`
Markers []mapMarker `json:"markers"`
Theme string `json:"theme"` // light|dark|auto (only when style is empty)
Geolocate bool `json:"geolocate"` // show MapLibre's GeolocateControl
Scale bool `json:"scale"` // show MapLibre's ScaleControl
// SearchURL is the same-origin geocode proxy. Set by New() to GeocodeURL when
// WithSearch is enabled; empty means map.js renders no search control.
SearchURL string `json:"searchURL"`
Cluster bool `json:"cluster"`
ClusterRadius float64 `json:"clusterRadius"`
ClusterMaxZoom float64 `json:"clusterMaxZoom"`
}
MapConfig is the map configuration serialized into the mount element's data-config attribute. Every field is always serialized (no omitempty) so map.js always receives a complete config and never has to guess a default. The With* options above set individual slots.
type MountConfig ¶
type MountConfig struct {
DocID string
DocField string // hidden input name for the canonical doc JSON (default "map_doc")
MinHeight string
Doc string // optional initial {lat,lng,zoom,markers} JSON, server-rendered for reload round-trip
}
MountConfig configures Plugin.Mount.
type Option ¶
type Option func(*Plugin)
Option configures a Plugin.
func WithCapabilities ¶
WithCapabilities overrides the grant set advertised to the host. Default: DefaultCapabilities.
func WithCenter ¶
WithCenter sets the default map center (lat, lng).
func WithClusterMaxZoom ¶
WithClusterMaxZoom sets the zoom above which pins stop clustering (default 14).
func WithClusterRadius ¶
WithClusterRadius sets the cluster radius in pixels (default 50).
func WithClustering ¶
func WithClustering() Option
WithClustering renders pins as counted cluster bubbles at low zoom instead of one marker each. Off by default. Clusters are DOM markers (not circle/symbol layers), so individual pins stay draggable and editable and no style glyphs are required — see js/src/map.ts.
func WithDemoPage ¶
func WithDemoPage() Option
WithDemoPage registers the self-contained themed demo page at DemoURL.
func WithDevGrantAll ¶
func WithDevGrantAll() Option
WithDevGrantAll bypasses the auth.HasScope capability gate so the demo / tests run without standing up auth. Default OFF (enforcing).
func WithGeocodeEndpoint ¶
WithGeocodeEndpoint points the built-in Nominatim proxy at a different Nominatim-compatible search endpoint (a self-hosted instance, or a mirror). The endpoint is fixed at configuration time and only the `q` parameter is user-controlled, so this is not an SSRF surface. Implies WithSearch.
func WithGeocodeUserAgent ¶
WithGeocodeUserAgent sets the User-Agent sent upstream. Nominatim's usage policy REQUIRES a header that identifies your application and gives them a way to contact you — set this to something like "acme-maps/1.4 (+https://acme.example/contact)" for any real deployment. Implies WithSearch.
func WithGeocoder ¶
WithGeocoder replaces the lookup entirely and implies WithSearch. Use it to plug in a commercial geocoder, an internal place index, or a fixed dataset (which is how the example app keeps its e2e run offline). When set, none of the Nominatim machinery — endpoint, User-Agent, rate limit — is used; caching still applies.
func WithMapConfig ¶
WithMapConfig replaces the full default MapConfig. Use the field-specific options above for ergonomics; this is the escape hatch.
func WithMarkers ¶
func WithMarkers(markers []mapMarker) Option
WithMarkers seeds the map with the given markers on first mount.
func WithReadOnly ¶
func WithReadOnly() Option
WithReadOnly mounts the map read-only by default (click-to-add and marker dragging disabled).
func WithSaveHandler ¶
func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option
WithSaveHandler overrides the persistence hook. The default stores the canonical {lat,lng,zoom,markers} doc in an in-memory map keyed by DocID.
func WithSearch ¶
func WithSearch() Option
WithSearch enables the place-search control and registers GeocodeURL. Without any of the options below it proxies the public Nominatim endpoint under that service's usage policy (identifying User-Agent, 1 req/s, cached). Read the policy before pointing a production app at the public instance: https://operations.osmfoundation.org/policies/nominatim/
func WithStyle ¶
WithStyle sets the default base style. It is either an OpenFreeMap style name ("liberty", "positron", "dark", "bright", "fiord") or a full style URL. The default is "liberty". An empty value is rejected at New() — a Go-configured map always ships an explicit style (the JS theme-derivation path is a fallback for hand-written mounts only).
func WithStyleBaseURL ¶
WithStyleBaseURL is the CDN / self-host hook: the base URL joined onto a style NAME to form the style URL (default https://tiles.openfreemap.org/styles/). A host self-hosting OpenFreeMap or fronting it with their own CDN points this at their base and allows THAT host in their page CSP instead.
func WithStyles ¶
WithStyles sets the options offered by the in-map style switcher (the "layers" control). Defaults to ["liberty","positron","dark"]. Each must be non-empty.
func WithTheme ¶
WithTheme sets the default theme strategy: "light", "dark", or "auto". Only consulted by map.js when no explicit style is set; the Go default always ships an explicit style ("liberty"). Default "auto".
func WithoutGeolocateControl ¶
func WithoutGeolocateControl() Option
WithoutGeolocateControl hides MapLibre's GeolocateControl ("find me"). The control is shown by default; it never prompts for location permission on load, only on an explicit user click.
func WithoutScaleControl ¶
func WithoutScaleControl() Option
WithoutScaleControl hides MapLibre's ScaleControl (shown by default).
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the interactive vector-map plugin. It implements framework.Plugin and mirrors the tour plugin's shape: trusted host-page (no sandbox, no broker), capability gate, in-memory doc store with a save handler hook.
func New ¶
New constructs a Plugin. There is no platform manifest (this is a trusted host-page plugin — no sandbox, no broker). Style config is sanity-checked (non-empty Style and Styles entries) so a misconfiguration fails loud at construction; a wrong-but-non-empty style name is left alone (it just 404s a tile, which is non-fatal).
func (*Plugin) Capabilities ¶
func (*Plugin) DefaultConfig ¶
DefaultConfig returns the map-config defaults this plugin instance will advertise (set via the With* options). map.js receives these through the mount element's data-config attribute.
func (*Plugin) Init ¶
Init registers the host-page assets, the save endpoint, and (optionally) the demo page on the app's router. The assets are NON-framed (trusted host-page scripts), so the AssetServer emits them with no CORP / frame-ancestors relaxation — just correct Content-Types. There is no broker route and no tile proxy: MapLibre fetches OpenFreeMap directly from the host page.
func (*Plugin) LoadDoc ¶
LoadDoc returns the last-saved canonical {lat,lng,zoom,markers} JSON for docID from the in-memory default store. ok is false when the doc has never been saved. The returned docJSON is the canonical interchange blob (schema map-v1) with the lowercase json tags map.js reads.
func (*Plugin) Mount ¶
func (p *Plugin) Mount(cfg MountConfig) render.HTML
Mount renders a plain host-page mount element plus the hidden input map.js mirrors the canonical doc JSON into. It does NOT use the platform pluginhost.MountMarker (that builds the sandboxed-iframe broker marker) — this is a trusted host-page mount, so a plain <div data-fui-geomap ...> is enough; map.js finds it on DOMContentLoaded and constructs the MapLibre map. Drop it into any form. All interpolated values are HTML-escaped via render.Escape.
The instance's configured MapConfig (set via the With* options) is serialized into data-config; the saved doc (if any) into data-doc, which OVERRIDES config center/zoom/markers on reload.