geomap

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 16 Imported by: 0

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

View Source
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 "/".

View Source
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

View Source
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

func UIHostOption() uihost.Option

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

func WithCapabilities(caps ...string) Option

WithCapabilities overrides the grant set advertised to the host. Default: DefaultCapabilities.

func WithCenter

func WithCenter(lat, lng float64) Option

WithCenter sets the default map center (lat, lng).

func WithClusterMaxZoom

func WithClusterMaxZoom(z float64) Option

WithClusterMaxZoom sets the zoom above which pins stop clustering (default 14).

func WithClusterRadius

func WithClusterRadius(px float64) Option

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

func WithGeocodeEndpoint(endpoint string) Option

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

func WithGeocodeUserAgent(ua string) Option

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

func WithGeocoder(fn Geocoder) Option

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

func WithMapConfig(cfg MapConfig) Option

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 WithMaxZoom

func WithMaxZoom(z float64) Option

WithMaxZoom sets the maximum zoom level.

func WithMinZoom

func WithMinZoom(z float64) Option

WithMinZoom sets the minimum zoom level.

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

func WithStyle(name string) Option

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

func WithStyleBaseURL(url string) Option

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

func WithStyles(names ...string) Option

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

func WithTheme(theme string) Option

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 WithZoom

func WithZoom(z float64) Option

WithZoom sets the default zoom level (0..22).

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

func New(opts ...Option) *Plugin

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 (p *Plugin) Capabilities() []string

func (*Plugin) DefaultConfig

func (p *Plugin) DefaultConfig() MapConfig

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

func (p *Plugin) Init(app *framework.App) error

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

func (p *Plugin) LoadDoc(ctx context.Context, docID string) (docJSON string, ok bool)

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.

func (*Plugin) Name

func (p *Plugin) Name() string

type SaveRequest

type SaveRequest struct {
	DocID         string
	Lat           float64
	Lng           float64
	Zoom          float64
	Markers       []mapMarker
	SchemaVersion string
}

SaveRequest is the persistence payload handed to the save handler.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL