- Export Signal type (signal → Signal) so subpackages can reference it - Expose viewport signals as public fields (CenterLng, CenterLat, Zoom, Bearing, Pitch) for .Text() display and .Bind() usage - Add signal-backed marker positions (LngSignal/LatSignal) with data-effect reactivity for server push and dragend writeback - Add event system (MapEvent, OnClick, OnLayerClick, OnMouseMove, OnContextMenu) using hidden inputs + action triggers - Add Source interface replacing type-switch, with RawSource escape hatch - Add CameraOptions for FlyTo/EaseTo/JumpTo/FitBounds/Stop - Add Control interface with NavigationControl, ScaleControl, GeolocateControl, FullscreenControl - Expand Options with interaction toggles, MaxBounds, and Extra map - Add effectspike example to validate data-effect with server-pushed signals - Update maplibre example to showcase all new features
360 lines
7.5 KiB
Go
360 lines
7.5 KiB
Go
package maplibre
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/ryanhamamura/via"
|
|
)
|
|
|
|
// LngLat represents a geographic coordinate.
|
|
type LngLat struct {
|
|
Lng float64
|
|
Lat float64
|
|
}
|
|
|
|
// LngLatBounds represents a rectangular geographic area.
|
|
type LngLatBounds struct {
|
|
SW LngLat
|
|
NE LngLat
|
|
}
|
|
|
|
// Padding represents padding in pixels on each side of the map viewport.
|
|
type Padding struct {
|
|
Top int
|
|
Bottom int
|
|
Left int
|
|
Right int
|
|
}
|
|
|
|
// Options configures the initial map state.
|
|
type Options struct {
|
|
// Style is the map style URL (required).
|
|
Style string
|
|
|
|
Center LngLat
|
|
Zoom float64
|
|
Bearing float64
|
|
Pitch float64
|
|
MinZoom float64
|
|
MaxZoom float64
|
|
|
|
// CSS dimensions for the map container. Defaults: "100%", "400px".
|
|
Width string
|
|
Height string
|
|
|
|
// Interaction toggles (nil = MapLibre default)
|
|
ScrollZoom *bool
|
|
BoxZoom *bool
|
|
DragRotate *bool
|
|
DragPan *bool
|
|
Keyboard *bool
|
|
DoubleClickZoom *bool
|
|
TouchZoomRotate *bool
|
|
TouchPitch *bool
|
|
RenderWorldCopies *bool
|
|
|
|
MaxBounds *LngLatBounds
|
|
|
|
// Extra is merged last into the MapLibre constructor options object,
|
|
// allowing pass-through of any option not covered above.
|
|
Extra map[string]any
|
|
}
|
|
|
|
// --- Source interface ---
|
|
|
|
// Source is implemented by map data sources (GeoJSON, vector, raster, etc.).
|
|
type Source interface {
|
|
sourceJS() string
|
|
}
|
|
|
|
// GeoJSONSource provides inline GeoJSON data to MapLibre.
|
|
// Data should be a GeoJSON-marshalable value (struct, map, or json.RawMessage).
|
|
type GeoJSONSource struct {
|
|
Data any
|
|
}
|
|
|
|
func (s GeoJSONSource) sourceJS() string {
|
|
data, _ := json.Marshal(s.Data)
|
|
return `{"type":"geojson","data":` + string(data) + `}`
|
|
}
|
|
|
|
// VectorSource references a vector tile source.
|
|
type VectorSource struct {
|
|
URL string
|
|
Tiles []string
|
|
}
|
|
|
|
func (s VectorSource) sourceJS() string {
|
|
obj := map[string]any{"type": "vector"}
|
|
if s.URL != "" {
|
|
obj["url"] = s.URL
|
|
}
|
|
if len(s.Tiles) > 0 {
|
|
obj["tiles"] = s.Tiles
|
|
}
|
|
b, _ := json.Marshal(obj)
|
|
return string(b)
|
|
}
|
|
|
|
// RasterSource references a raster tile source.
|
|
type RasterSource struct {
|
|
URL string
|
|
Tiles []string
|
|
TileSize int
|
|
}
|
|
|
|
func (s RasterSource) sourceJS() string {
|
|
obj := map[string]any{"type": "raster"}
|
|
if s.URL != "" {
|
|
obj["url"] = s.URL
|
|
}
|
|
if len(s.Tiles) > 0 {
|
|
obj["tiles"] = s.Tiles
|
|
}
|
|
if s.TileSize > 0 {
|
|
obj["tileSize"] = s.TileSize
|
|
}
|
|
b, _ := json.Marshal(obj)
|
|
return string(b)
|
|
}
|
|
|
|
// RawSource is an escape hatch that passes an arbitrary JSON-marshalable
|
|
// value directly as a MapLibre source definition.
|
|
type RawSource struct {
|
|
Value any
|
|
}
|
|
|
|
func (s RawSource) sourceJS() string {
|
|
b, _ := json.Marshal(s.Value)
|
|
return string(b)
|
|
}
|
|
|
|
// --- Control interface ---
|
|
|
|
// Control is implemented by map controls (navigation, scale, etc.).
|
|
type Control interface {
|
|
controlJS() string
|
|
controlPosition() string
|
|
}
|
|
|
|
// NavigationControl adds zoom and rotation buttons.
|
|
type NavigationControl struct {
|
|
Position string // "top-right" (default), "top-left", "bottom-right", "bottom-left"
|
|
ShowCompass *bool
|
|
ShowZoom *bool
|
|
VisualizeRoll *bool
|
|
VisualizePitch *bool
|
|
}
|
|
|
|
func (c NavigationControl) controlJS() string {
|
|
opts := map[string]any{}
|
|
if c.ShowCompass != nil {
|
|
opts["showCompass"] = *c.ShowCompass
|
|
}
|
|
if c.ShowZoom != nil {
|
|
opts["showZoom"] = *c.ShowZoom
|
|
}
|
|
if c.VisualizeRoll != nil {
|
|
opts["visualizeRoll"] = *c.VisualizeRoll
|
|
}
|
|
if c.VisualizePitch != nil {
|
|
opts["visualizePitch"] = *c.VisualizePitch
|
|
}
|
|
b, _ := json.Marshal(opts)
|
|
return "new maplibregl.NavigationControl(" + string(b) + ")"
|
|
}
|
|
|
|
func (c NavigationControl) controlPosition() string {
|
|
if c.Position == "" {
|
|
return "top-right"
|
|
}
|
|
return c.Position
|
|
}
|
|
|
|
// ScaleControl displays a scale bar.
|
|
type ScaleControl struct {
|
|
Position string // default "bottom-left"
|
|
MaxWidth int
|
|
Unit string // "metric", "imperial", "nautical"
|
|
}
|
|
|
|
func (c ScaleControl) controlJS() string {
|
|
opts := map[string]any{}
|
|
if c.MaxWidth > 0 {
|
|
opts["maxWidth"] = c.MaxWidth
|
|
}
|
|
if c.Unit != "" {
|
|
opts["unit"] = c.Unit
|
|
}
|
|
b, _ := json.Marshal(opts)
|
|
return "new maplibregl.ScaleControl(" + string(b) + ")"
|
|
}
|
|
|
|
func (c ScaleControl) controlPosition() string {
|
|
if c.Position == "" {
|
|
return "bottom-left"
|
|
}
|
|
return c.Position
|
|
}
|
|
|
|
// GeolocateControl adds a button to track the user's location.
|
|
type GeolocateControl struct {
|
|
Position string // default "top-right"
|
|
}
|
|
|
|
func (c GeolocateControl) controlJS() string {
|
|
return "new maplibregl.GeolocateControl()"
|
|
}
|
|
|
|
func (c GeolocateControl) controlPosition() string {
|
|
if c.Position == "" {
|
|
return "top-right"
|
|
}
|
|
return c.Position
|
|
}
|
|
|
|
// FullscreenControl adds a fullscreen toggle button.
|
|
type FullscreenControl struct {
|
|
Position string // default "top-right"
|
|
}
|
|
|
|
func (c FullscreenControl) controlJS() string {
|
|
return "new maplibregl.FullscreenControl()"
|
|
}
|
|
|
|
func (c FullscreenControl) controlPosition() string {
|
|
if c.Position == "" {
|
|
return "top-right"
|
|
}
|
|
return c.Position
|
|
}
|
|
|
|
// --- Camera options ---
|
|
|
|
// CameraOptions configures animated camera movements (FlyTo, EaseTo, JumpTo).
|
|
// Nil pointer fields are omitted from the JS call.
|
|
type CameraOptions struct {
|
|
Center *LngLat
|
|
Zoom *float64
|
|
Bearing *float64
|
|
Pitch *float64
|
|
Duration *int // milliseconds
|
|
Speed *float64 // FlyTo only
|
|
Curve *float64 // FlyTo only
|
|
Padding *Padding
|
|
Animate *bool
|
|
}
|
|
|
|
// --- Layer ---
|
|
|
|
// Layer describes a MapLibre style layer.
|
|
type Layer struct {
|
|
ID string
|
|
Type string
|
|
Source string
|
|
SourceLayer string
|
|
Paint map[string]any
|
|
Layout map[string]any
|
|
Filter any
|
|
MinZoom float64
|
|
MaxZoom float64
|
|
|
|
// Before inserts this layer before the given layer ID in the stack.
|
|
Before string
|
|
}
|
|
|
|
func (l Layer) toJS() string {
|
|
obj := map[string]any{
|
|
"id": l.ID,
|
|
"type": l.Type,
|
|
}
|
|
if l.Source != "" {
|
|
obj["source"] = l.Source
|
|
}
|
|
if l.SourceLayer != "" {
|
|
obj["source-layer"] = l.SourceLayer
|
|
}
|
|
if l.Paint != nil {
|
|
obj["paint"] = l.Paint
|
|
}
|
|
if l.Layout != nil {
|
|
obj["layout"] = l.Layout
|
|
}
|
|
if l.Filter != nil {
|
|
obj["filter"] = l.Filter
|
|
}
|
|
if l.MinZoom > 0 {
|
|
obj["minzoom"] = l.MinZoom
|
|
}
|
|
if l.MaxZoom > 0 {
|
|
obj["maxzoom"] = l.MaxZoom
|
|
}
|
|
b, _ := json.Marshal(obj)
|
|
return string(b)
|
|
}
|
|
|
|
// --- Marker ---
|
|
|
|
// Marker describes a map marker.
|
|
type Marker struct {
|
|
LngLat LngLat // static position (used when signals are nil)
|
|
Color string
|
|
Draggable bool
|
|
Popup *Popup
|
|
|
|
// Signal-backed position. When set, signals drive marker position reactively.
|
|
// Initial position is read from the signal values. LngLat is ignored when signals are set.
|
|
// If Draggable is true, drag updates write back to these signals.
|
|
LngSignal *via.Signal
|
|
LatSignal *via.Signal
|
|
}
|
|
|
|
// Popup describes a map popup.
|
|
//
|
|
// Content is rendered as HTML via MapLibre's setHTML. Do not pass untrusted
|
|
// user input without sanitizing it first.
|
|
type Popup struct {
|
|
Content string // HTML content
|
|
LngLat LngLat
|
|
HideCloseButton bool // true removes the close button (MapLibre shows it by default)
|
|
MaxWidth string
|
|
}
|
|
|
|
// --- Event data ---
|
|
|
|
// EventData contains data from a map event (click, mousemove, etc.).
|
|
type EventData struct {
|
|
LngLat LngLat `json:"lngLat"`
|
|
Point [2]float64 `json:"point"`
|
|
Features []json.RawMessage `json:"features,omitempty"`
|
|
LayerID string `json:"layerID,omitempty"`
|
|
}
|
|
|
|
// --- Internal accumulation entries ---
|
|
|
|
type sourceEntry struct {
|
|
id string
|
|
js string
|
|
}
|
|
|
|
type markerEntry struct {
|
|
id string
|
|
marker Marker
|
|
}
|
|
|
|
type popupEntry struct {
|
|
id string
|
|
popup Popup
|
|
}
|
|
|
|
type eventEntry struct {
|
|
event string
|
|
layerID string
|
|
signal *via.Signal
|
|
}
|
|
|
|
type controlEntry struct {
|
|
id string
|
|
ctrl Control
|
|
}
|