Add GeoJSON point containment helper
This commit is contained in:
107
internal/geo/geojson.go
Normal file
107
internal/geo/geojson.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type geometry struct {
|
||||
Type string `json:"type"`
|
||||
Coordinates json.RawMessage `json:"coordinates"`
|
||||
}
|
||||
|
||||
// ContainsPoint reports whether a GeoJSON Polygon or MultiPolygon contains p.
|
||||
// GeoJSON coordinate order is [longitude, latitude].
|
||||
func ContainsPoint(raw []byte, p Point) (bool, error) {
|
||||
if len(raw) == 0 {
|
||||
return false, fmt.Errorf("geojson geometry is empty")
|
||||
}
|
||||
|
||||
var geom geometry
|
||||
if err := json.Unmarshal(raw, &geom); err != nil {
|
||||
return false, fmt.Errorf("decode geojson geometry: %w", err)
|
||||
}
|
||||
|
||||
switch geom.Type {
|
||||
case "Polygon":
|
||||
polygon, err := decodePolygon(geom.Coordinates)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("decode polygon: %w", err)
|
||||
}
|
||||
return polygonContainsPoint(polygon, p), nil
|
||||
case "MultiPolygon":
|
||||
multiPolygon, err := decodeMultiPolygon(geom.Coordinates)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("decode multipolygon: %w", err)
|
||||
}
|
||||
for _, polygon := range multiPolygon {
|
||||
if polygonContainsPoint(polygon, p) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
case "":
|
||||
return false, fmt.Errorf("geojson geometry type is required")
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported geojson geometry type %q", geom.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func decodePolygon(raw json.RawMessage) (Polygon, error) {
|
||||
var coords [][][]float64
|
||||
if err := json.Unmarshal(raw, &coords); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return polygonFromCoordinates(coords)
|
||||
}
|
||||
|
||||
func decodeMultiPolygon(raw json.RawMessage) ([]Polygon, error) {
|
||||
var coords [][][][]float64
|
||||
if err := json.Unmarshal(raw, &coords); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(coords) == 0 {
|
||||
return nil, fmt.Errorf("multipolygon has no polygons")
|
||||
}
|
||||
|
||||
out := make([]Polygon, 0, len(coords))
|
||||
for i, polygonCoords := range coords {
|
||||
polygon, err := polygonFromCoordinates(polygonCoords)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("polygons[%d]: %w", i, err)
|
||||
}
|
||||
out = append(out, polygon)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func polygonFromCoordinates(coords [][][]float64) (Polygon, error) {
|
||||
if len(coords) == 0 {
|
||||
return nil, fmt.Errorf("polygon has no rings")
|
||||
}
|
||||
|
||||
polygon := make(Polygon, 0, len(coords))
|
||||
for i, ringCoords := range coords {
|
||||
ring, err := ringFromCoordinates(ringCoords)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rings[%d]: %w", i, err)
|
||||
}
|
||||
polygon = append(polygon, ring)
|
||||
}
|
||||
return polygon, nil
|
||||
}
|
||||
|
||||
func ringFromCoordinates(coords [][]float64) (Ring, error) {
|
||||
if len(coords) == 0 {
|
||||
return nil, fmt.Errorf("ring has no points")
|
||||
}
|
||||
|
||||
ring := make(Ring, 0, len(coords))
|
||||
for i, pair := range coords {
|
||||
if len(pair) < 2 {
|
||||
return nil, fmt.Errorf("points[%d] has %d values, need longitude and latitude", i, len(pair))
|
||||
}
|
||||
ring = append(ring, Point{Longitude: pair[0], Latitude: pair[1]})
|
||||
}
|
||||
return ring, nil
|
||||
}
|
||||
Reference in New Issue
Block a user