arcgis

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: MIT Imports: 9 Imported by: 0

README

go-arcgis

Go Reference Go

A small, dependency-free Go client for querying ArcGIS Feature Services over their REST API. It handles pagination, counts, and object-ID-only queries, and offers two interchangeable styles: a plain struct API and a fluent builder.

client := arcgis.NewClient(baseURL)

features, err := client.Layer(7).Query().
    Where("STAGE = 4").
    Fields("BLOCK_NAME", "STAGE").
    All(ctx) // paginates automatically

Install

go get github.com/richardwooding/go-arcgis

Requires Go 1.26+. No third-party dependencies.

Two styles, one engine

The fluent builder and the struct API produce the same QueryParams and hit the same code path — pick whichever reads better at the call site.

Struct style — explicit and serializable
fs, err := client.Query(ctx, arcgis.QueryParams{
    LayerID:  7,
    Where:    "STAGE = 4",
    Fields:   []string{"BLOCK_NAME", "STAGE"},
    PageSize: 100,
})
Fluent style — readable and chainable
features, err := client.Layer(7).Query().
    Where("STAGE = 4").
    Fields("BLOCK_NAME", "STAGE").
    WithinEnvelope(18.4, -34.0, 18.6, -33.8).
    All(ctx)

Querying

Call Returns Notes
Query / .First *FeatureSet a single page
QueryAll / .All []Feature follows exceededTransferLimit until exhausted
QueryCount / .Count int no feature data transferred
QueryIDs / .IDs []int64 object IDs only

Pagination is automatic: QueryAll keeps advancing resultOffset while the service reports exceededTransferLimit.

Attributes are exposed uniformly regardless of the response format — Feature.Attrs() returns the Esri-JSON attributes map, falling back to the GeoJSON properties map:

for _, f := range features {
    fmt.Println(f.Attrs()["BLOCK_NAME"])
}

Spatial filters

// Bounding box (lon/lat)
client.Layer(7).Query().WithinEnvelope(18.4, -34.0, 18.6, -33.8)

// Point with an explicit relationship
client.Layer(7).Query().
    IntersectsPoint(18.42, -33.92).
    SpatialRel(arcgis.SpatialRelWithin)

Output format

GeoJSON is the default. Switch to Esri JSON when you need its richer metadata (objectIdFieldName, field definitions):

client.Layer(7).Query().Format(arcgis.FormatJSON).First(ctx)

Count and IDs always use Esri JSON internally, since that is the only format ArcGIS returns those responses in.

Options

arcgis.NewClient(baseURL,
    arcgis.WithToken("…"),                 // authenticated services
    arcgis.WithTimeout(10*time.Second),    // or:
    arcgis.WithHTTPClient(myClient),       // bring your own transport
)

A token, when set, is appended to every request — queries, counts, service and layer metadata.

Errors

ArcGIS frequently reports failures with an HTTP 200 status and an error envelope in the body. go-arcgis surfaces these as *arcgis.APIError:

_, err := client.Query(ctx, p)
var apiErr *arcgis.APIError
if errors.As(err, &apiErr) {
    fmt.Println(apiErr.Code, apiErr.Message)
}

Named services

Build named layer IDs and pre-built QueryParams for a specific service, then extend them at the call site with .From(...):

func loadSheddingForStage(stage int) arcgis.QueryParams {
    return arcgis.QueryParams{
        LayerID: 111,
        Where:   fmt.Sprintf("STAGE = %d", stage),
        Fields:  []string{"BLOCK_NAME", "STAGE"},
    }
}

features, err := client.Layer(111).Query().
    From(loadSheddingForStage(4)).
    WithinEnvelope(18.4, -34.0, 18.6, -33.8).
    All(ctx)

For the City of Cape Town Open Data Portal, the companion package capetown-opendata ships these constructors and verified layer IDs ready to use.

Changelog

See CHANGELOG.md.

License

MIT

Documentation

Overview

Package arcgis is a small, dependency-free client for querying ArcGIS Feature Services over their REST API.

It offers two interchangeable styles: a struct-based API (QueryParams passed to Client.Query) and a fluent builder (Client.LayerLayerClient.Query). Pagination, counts, and object-ID-only queries are first-class.

client := arcgis.NewClient(baseURL)
features, err := client.Layer(7).Query().
	Where("STAGE = 4").
	Fields("NAME", "STAGE").
	All(ctx)
Example (CountOnly)
package main

import (
	"context"
	"fmt"
	"log"

	arcgis "github.com/richardwooding/go-arcgis"
)

// baseURL is an example FeatureServer root. Replace it with the service you
// are querying.
const baseURL = "https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer"

func main() {
	client := arcgis.NewClient(baseURL)
	ctx := context.Background()

	// Just get a count — no feature data transferred
	count, err := client.Layer(1).
		Query().
		Where("SUBURB = 'Woodstock'").
		Count(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Matching records: %d\n", count)
}
Example (FluentStyle)
package main

import (
	"context"
	"fmt"
	"log"

	arcgis "github.com/richardwooding/go-arcgis"
)

// baseURL is an example FeatureServer root. Replace it with the service you
// are querying.
const baseURL = "https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer"

func main() {
	client := arcgis.NewClient(baseURL)
	ctx := context.Background()

	// Fluent style — readable, chainable
	features, err := client.Layer(111).
		Query().
		Where("STAGE = 4").
		Fields("BLOCK_NAME", "STAGE").
		WithinEnvelope(18.4, -34.0, 18.6, -33.8).
		PageSize(100).
		All(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(features))
}
Example (InspectParams)
package main

import (
	"fmt"

	arcgis "github.com/richardwooding/go-arcgis"
)

// baseURL is an example FeatureServer root. Replace it with the service you
// are querying.
const baseURL = "https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer"

func main() {
	client := arcgis.NewClient(baseURL)

	// QueryParams is just a struct — inspect, log, or marshal it
	p := client.Layer(64).
		Query().
		Where("YEAR = 2021").
		WithoutGeometry().
		Params() // returns QueryParams without executing

	fmt.Printf("Layer: %d, Where: %s\n", p.LayerID, p.Where)
}
Output:
Layer: 64, Where: YEAR = 2021
Example (NamedQuery)
package main

import (
	"context"
	"fmt"
	"log"

	arcgis "github.com/richardwooding/go-arcgis"
)

// baseURL is an example FeatureServer root. Replace it with the service you
// are querying.
const baseURL = "https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer"

// loadSheddingForStage is the kind of named, pre-built query an application
// or companion package might expose.
func loadSheddingForStage(stage int) arcgis.QueryParams {
	return arcgis.QueryParams{
		LayerID: 111,
		Where:   fmt.Sprintf("STAGE = %d", stage),
		Fields:  []string{"BLOCK_NAME", "STAGE"},
	}
}

func main() {
	client := arcgis.NewClient(baseURL)
	ctx := context.Background()

	// Named query — pre-built params, optionally extended
	features, err := client.Layer(111).
		Query().
		From(loadSheddingForStage(4)).
		WithinEnvelope(18.4, -34.0, 18.6, -33.8). // further refined
		All(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(features))
}
Example (PaginatedAll)
package main

import (
	"context"
	"fmt"
	"log"

	arcgis "github.com/richardwooding/go-arcgis"
)

// baseURL is an example FeatureServer root. Replace it with the service you
// are querying.
const baseURL = "https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer"

func main() {
	client := arcgis.NewClient(baseURL)
	ctx := context.Background()

	// QueryAll handles pagination transparently
	all, err := client.QueryAll(ctx, arcgis.QueryParams{LayerID: 229})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Total records: %d\n", len(all))
}
Example (StructStyle)
package main

import (
	"context"
	"fmt"
	"log"

	arcgis "github.com/richardwooding/go-arcgis"
)

// baseURL is an example FeatureServer root. Replace it with the service you
// are querying.
const baseURL = "https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer"

func main() {
	client := arcgis.NewClient(baseURL)
	ctx := context.Background()

	// Struct-style — explicit, composable, serializable
	features, err := client.Query(ctx, arcgis.QueryParams{
		LayerID:  111,
		Where:    "STAGE = 4",
		Fields:   []string{"BLOCK_NAME", "STAGE"},
		PageSize: 100,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(features.Features))
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	Code    int      `json:"code"`
	Message string   `json:"message"`
	Details []string `json:"details,omitempty"`
}

APIError is an error returned by an ArcGIS service. ArcGIS commonly reports failures with an HTTP 200 status and an error envelope in the body, so this is surfaced even on otherwise-successful responses.

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the main entry point for interacting with an ArcGIS Feature Service.

func NewClient

func NewClient(baseURL string, opts ...ClientOption) *Client

NewClient creates a new ArcGIS Feature Service client. baseURL should be the root of the FeatureServer, e.g.:

https://example.gov/arcgis/rest/services/Theme/Service/FeatureServer

func (*Client) Layer

func (c *Client) Layer(id int) *LayerClient

Layer returns a LayerClient scoped to a specific layer ID.

func (*Client) LayerInfo

func (c *Client) LayerInfo(ctx context.Context, layerID int) (*LayerInfo, error)

LayerInfo fetches metadata for a specific layer.

func (*Client) Query

func (c *Client) Query(ctx context.Context, p QueryParams) (*FeatureSet, error)

Query executes a single-page query and returns the raw FeatureSet.

func (*Client) QueryAll

func (c *Client) QueryAll(ctx context.Context, p QueryParams) ([]Feature, error)

QueryAll paginates through all results and returns every Feature.

func (*Client) QueryCount

func (c *Client) QueryCount(ctx context.Context, p QueryParams) (int, error)

QueryCount returns the count of features matching the query.

func (*Client) QueryIDs

func (c *Client) QueryIDs(ctx context.Context, p QueryParams) ([]int64, error)

QueryIDs returns all object IDs matching the query.

func (*Client) ServiceInfo

func (c *Client) ServiceInfo(ctx context.Context) (*ServiceInfo, error)

ServiceInfo fetches metadata about the feature service.

type ClientOption

type ClientOption func(*Client)

ClientOption configures a Client.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) ClientOption

WithHTTPClient sets a custom HTTP client.

func WithTimeout

func WithTimeout(d time.Duration) ClientOption

WithTimeout sets a request timeout on the default HTTP client. It has no effect when combined with WithHTTPClient.

func WithToken

func WithToken(token string) ClientOption

WithToken sets an ArcGIS authentication token, appended to every request.

type Envelope

type Envelope struct {
	MinX float64
	MinY float64
	MaxX float64
	MaxY float64
}

Envelope is a bounding-box spatial filter, expressed in the layer's coordinate system (typically WGS84 longitude/latitude).

type Feature

type Feature struct {
	Geometry   json.RawMessage `json:"geometry"`
	Attributes map[string]any  `json:"attributes,omitempty"` // Esri JSON
	Properties map[string]any  `json:"properties,omitempty"` // GeoJSON
}

Feature represents a single ArcGIS feature with geometry and attributes.

The geometry is left as raw JSON because its shape depends on the requested format (Esri JSON vs GeoJSON) and the layer's geometry type. Attributes are populated for Esri JSON responses; Properties for GeoJSON.

func (Feature) Attrs

func (f Feature) Attrs() map[string]any

Attrs returns the feature's attribute map regardless of response format, preferring Esri JSON attributes and falling back to GeoJSON properties.

type FeatureSet

type FeatureSet struct {
	Features              []Feature `json:"features"`
	ExceededTransferLimit bool      `json:"exceededTransferLimit"`
	ObjectIDFieldName     string    `json:"objectIdFieldName,omitempty"`
	Fields                []Field   `json:"fields,omitempty"`
}

FeatureSet is a collection of features returned from a query.

type Field

type Field struct {
	Name  string `json:"name"`
	Type  string `json:"type"`
	Alias string `json:"alias"`
}

Field describes a single attribute field in a layer.

type GeometryType

type GeometryType string

GeometryType for spatial filter inputs.

const (
	GeometryTypeEnvelope GeometryType = "esriGeometryEnvelope"
	GeometryTypePoint    GeometryType = "esriGeometryPoint"
	GeometryTypePolygon  GeometryType = "esriGeometryPolygon"
)

Supported geometry types for spatial filter inputs.

type LayerClient

type LayerClient struct {
	// contains filtered or unexported fields
}

LayerClient is scoped to a single layer and provides the fluent query entry point.

func (*LayerClient) Info

func (l *LayerClient) Info(ctx context.Context) (*LayerInfo, error)

Info fetches metadata for this layer.

func (*LayerClient) Query

func (l *LayerClient) Query() *QueryBuilder

Query returns a QueryBuilder pre-scoped to this layer.

type LayerInfo

type LayerInfo struct {
	ID                 int     `json:"id"`
	Name               string  `json:"name"`
	Type               string  `json:"type"`
	Description        string  `json:"description"`
	MaxRecordCount     int     `json:"maxRecordCount"`
	Fields             []Field `json:"fields"`
	GeometryType       string  `json:"geometryType"`
	SupportsStatistics bool    `json:"supportsStatistics"`
	SupportsPagination bool    `json:"supportsPagination"`
}

LayerInfo contains metadata about a feature layer.

type LayerRef

type LayerRef struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

LayerRef is a lightweight layer reference in a service listing.

type OutputFormat

type OutputFormat string

OutputFormat controls the response format from ArcGIS.

const (
	// FormatGeoJSON requests RFC 7946 GeoJSON. Each feature carries its
	// attributes under "properties".
	FormatGeoJSON OutputFormat = "geojson"
	// FormatJSON requests Esri JSON. Each feature carries its attributes
	// under "attributes".
	FormatJSON OutputFormat = "json"
	// FormatPBF requests the Protocol Buffer encoding. This package decodes
	// JSON responses only; use FormatPBF only with a custom decoder.
	FormatPBF OutputFormat = "pbf"
)

type Point

type Point struct {
	X float64
	Y float64
}

Point is a single coordinate.

type Polygon added in v0.2.0

type Polygon struct {
	Rings [][][]float64
}

Polygon is a polygon spatial filter, expressed as one or more linear rings of [x, y] coordinates in the layer's coordinate system (typically WGS84 longitude/latitude). Per the Esri convention an exterior ring is clockwise and holes are counter-clockwise, but ArcGIS query filters tolerate either.

type QueryBuilder

type QueryBuilder struct {
	// contains filtered or unexported fields
}

QueryBuilder builds a QueryParams using a fluent chainable API.

func (*QueryBuilder) All

func (q *QueryBuilder) All(ctx context.Context) ([]Feature, error)

All fetches all results, handling pagination automatically.

func (*QueryBuilder) Count

func (q *QueryBuilder) Count(ctx context.Context) (int, error)

Count returns only the record count matching the query.

func (*QueryBuilder) DistinctValues added in v0.1.1

func (q *QueryBuilder) DistinctValues() *QueryBuilder

DistinctValues requests only distinct values for the selected Fields.

func (*QueryBuilder) Fields

func (q *QueryBuilder) Fields(fields ...string) *QueryBuilder

Fields sets the output fields to return.

func (*QueryBuilder) First

func (q *QueryBuilder) First(ctx context.Context) (*FeatureSet, error)

First fetches only the first page of results.

func (*QueryBuilder) Format

func (q *QueryBuilder) Format(f OutputFormat) *QueryBuilder

Format sets the response format.

func (*QueryBuilder) From

func (q *QueryBuilder) From(base QueryParams) *QueryBuilder

From merges a pre-built QueryParams into this builder (useful for named queries). The layer ID set by Layer() is preserved.

func (*QueryBuilder) GroupBy

func (q *QueryBuilder) GroupBy(fields ...string) *QueryBuilder

GroupBy sets the GROUP BY fields (used with statistics queries).

func (*QueryBuilder) IDs

func (q *QueryBuilder) IDs(ctx context.Context) ([]int64, error)

IDs returns only the object IDs matching the query.

func (*QueryBuilder) InSR added in v0.2.0

func (q *QueryBuilder) InSR(wkid int) *QueryBuilder

InSR sets the spatial reference (well-known ID) of the input filter geometry.

func (*QueryBuilder) IntersectsPoint

func (q *QueryBuilder) IntersectsPoint(x, y float64) *QueryBuilder

IntersectsPoint sets a point spatial filter using the default "intersects" relationship.

func (*QueryBuilder) Offset

func (q *QueryBuilder) Offset(n int) *QueryBuilder

Offset sets the starting record offset for pagination.

func (*QueryBuilder) OrderBy

func (q *QueryBuilder) OrderBy(fields ...string) *QueryBuilder

OrderBy sets the ORDER BY fields.

func (*QueryBuilder) PageSize

func (q *QueryBuilder) PageSize(n int) *QueryBuilder

PageSize sets the number of records per page.

func (*QueryBuilder) Params

func (q *QueryBuilder) Params() QueryParams

Params returns the underlying QueryParams for inspection or reuse.

func (*QueryBuilder) SpatialRel

func (q *QueryBuilder) SpatialRel(rel SpatialRel) *QueryBuilder

SpatialRel overrides the spatial relationship applied to the geometry filter.

func (*QueryBuilder) Where

func (q *QueryBuilder) Where(clause string) *QueryBuilder

Where sets the SQL WHERE clause.

func (*QueryBuilder) WithinEnvelope

func (q *QueryBuilder) WithinEnvelope(minX, minY, maxX, maxY float64) *QueryBuilder

WithinEnvelope sets a bounding-box spatial filter.

func (*QueryBuilder) WithinPolygon added in v0.2.0

func (q *QueryBuilder) WithinPolygon(rings [][][]float64) *QueryBuilder

WithinPolygon sets a polygon spatial filter from one or more [x, y] rings.

func (*QueryBuilder) WithoutGeometry

func (q *QueryBuilder) WithoutGeometry() *QueryBuilder

WithoutGeometry omits geometry from the response (faster for attribute-only queries).

type QueryParams

type QueryParams struct {
	LayerID      int
	Where        string
	Fields       []string
	Envelope     *Envelope
	Geometry     *Point
	Polygon      *Polygon
	GeometryType GeometryType
	SpatialRel   SpatialRel
	// InSR is the well-known ID of the spatial reference the input geometry
	// (Envelope/Geometry/Polygon) is expressed in. When zero and a geometry
	// filter is set, it defaults to 4326 (WGS84 longitude/latitude) — the SR
	// of the coordinates callers normally supply. Without it, ArcGIS assumes
	// the geometry is in the layer's native SR, so a WGS84 box silently
	// matches nothing against a layer stored in Web Mercator.
	InSR            int
	OrderByFields   []string
	GroupByFields   []string
	ResultOffset    int
	PageSize        int
	ReturnGeometry  *bool // nil = server default (true)
	ReturnIDsOnly   bool
	ReturnCountOnly bool
	// ReturnDistinctValues requests only distinct values for the selected
	// Fields. Typically combined with Fields (and often OrderByFields) to
	// enumerate the values present in one or more columns.
	ReturnDistinctValues bool
	Format               OutputFormat
}

QueryParams defines all parameters for an ArcGIS feature query. It can be used directly (struct-style) or built via QueryBuilder (fluent-style).

The zero value is usable: defaults are applied for an unset Where clause ("1=1"), Format (GeoJSON), and PageSize (1000).

type ServiceInfo

type ServiceInfo struct {
	ServiceDescription string     `json:"serviceDescription"`
	Layers             []LayerRef `json:"layers"`
	Tables             []LayerRef `json:"tables"`
}

ServiceInfo contains metadata about a feature service.

type SpatialRel

type SpatialRel string

SpatialRel defines the spatial relationship for geometry filters.

const (
	SpatialRelIntersects         SpatialRel = "esriSpatialRelIntersects"
	SpatialRelContains           SpatialRel = "esriSpatialRelContains"
	SpatialRelWithin             SpatialRel = "esriSpatialRelWithin"
	SpatialRelTouches            SpatialRel = "esriSpatialRelTouches"
	SpatialRelOverlaps           SpatialRel = "esriSpatialRelOverlaps"
	SpatialRelEnvelopeIntersects SpatialRel = "esriSpatialRelEnvelopeIntersects"
)

Supported spatial relationships for geometry filters.

Jump to

Keyboard shortcuts

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