• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fogfish / geojson / 17354804133

31 Aug 2025 08:43AM UTC coverage: 87.368% (-1.3%) from 88.696%
17354804133

Pull #17

github

fogfish
enable direct Marshal / Unmarshal
Pull Request #17: Feature ID magagable within the domain model

41 of 48 new or added lines in 1 file covered. (85.42%)

1 existing line in 1 file now uncovered.

415 of 475 relevant lines covered (87.37%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.45
/feature.go
1
//
2
// Copyright (C) 2021 Dmitry Kolesnikov
3
//
4
// This file may be modified and distributed under the terms
5
// of the MIT license.  See the LICENSE file for details.
6
// https://github.com/fogfish/geojson
7
//
8

9
package geojson
10

11
import (
12
        "encoding/json"
13
)
14

15
const TYPE_FEATURE = "Feature"
16

17
// Feature object represents a spatially bounded thing.
18
// This object contains geometry, a common identifier, and properties.
19
// The value of the properties is any JSON object, typically defined by
20
// an application.
21
//
22
// The library uses a type safe notation for the feature's property
23
// definition instead of generic any type. It uses type tagging
24
// technique (or embedding):
25
//
26
//        type MyType struct {
27
//          geojson.Feature
28
//          Name      string `json:"name,omitempty"`
29
//        }
30
type Feature struct {
31
        Geometry Geometry `json:"-"`
32
}
33

34
func (fea Feature) unapply() Geometry { return fea.Geometry }
1✔
35
func (fea *Feature) apply(g Geometry) { fea.Geometry = g }
1✔
36

37
// Return the bounding box of the feature's geometry.
38
func (fea Feature) BoundingBox() BoundingBox {
1✔
39
        if fea.Geometry == nil {
2✔
40
                return nil
1✔
41
        }
1✔
42

43
        return fea.Geometry.BoundingBox()
1✔
44
}
45

46
// IFeature is a phantom type of the Feature itself, used for type-safe marshaling and unmarshaling.
47
type IFeature interface {
48
        BoundingBox() BoundingBox
49
        unapply() Geometry
50
        apply(Geometry)
51
}
52

53
//
54
// Encoder
55
//
56

57
// EncodeGeoJSON is a helper function to implement GeoJSON codec
58
//
59
//        func (x MyType) MarshalJSON() ([]byte, error) {
60
//          type tStruct MyType
61
//          return x.Feature.EncodeGeoJSON(x.ID, tStruct(x))
62
//        }
63
func (fea Feature) EncodeGeoJSON(props any) ([]byte, error) {
1✔
64
        return fencoder(&fea, props)
1✔
65
}
1✔
66

67
// Encodes object as GeoJSON
NEW
68
func MarshalJSON[T IFeature](obj T) ([]byte, error) {
×
NEW
69
        return fencoder(obj, obj)
×
NEW
70
}
×
71

72
func fencoder(fea IFeature, obj any) ([]byte, error) {
1✔
73
        geometry := fea.unapply()
1✔
74
        properties, err := json.Marshal(obj)
1✔
75
        if err != nil {
1✔
76
                return nil, err
×
77
        }
×
78

79
        // Note: skip bounding box for the point.
80
        var bbox BoundingBox
1✔
81
        switch geometry.(type) {
1✔
82
        case nil:
1✔
83
                bbox = nil
1✔
84
        case *Point:
1✔
85
                bbox = nil
1✔
86
        default:
1✔
87
                bbox = geometry.BoundingBox()
1✔
88
        }
89

90
        // ID is optional if the feature has a unique identifier.
91
        var id string
1✔
92
        if identity, has := any(obj).(interface{ GeoJsonID() string }); has {
1✔
NEW
93
                id = identity.GeoJsonID()
×
UNCOV
94
        }
×
95

96
        val := struct {
1✔
97
                Type       string          `json:"type"`
1✔
98
                ID         string          `json:"id,omitempty"`
1✔
99
                BBox       BoundingBox     `json:"bbox,omitempty"`
1✔
100
                Geometry   Geometry        `json:"geometry"`
1✔
101
                Properties json.RawMessage `json:"properties,omitempty"`
1✔
102
        }{
1✔
103
                Type:       TYPE_FEATURE,
1✔
104
                ID:         id,
1✔
105
                BBox:       bbox,
1✔
106
                Geometry:   geometry,
1✔
107
                Properties: properties,
1✔
108
        }
1✔
109

1✔
110
        return json.Marshal(val)
1✔
111
}
112

113
//
114
// Decoder
115
//
116

117
// DecodeGeoJSON is a helper function to implement GeoJSON codec
118
//
119
//        func (x *MyType) UnmarshalJSON(b []byte) (err error) {
120
//          type tStruct *MyType
121
//          x.ID, err = x.Feature.DecodeGeoJSON(b, tStruct(x))
122
//          return
123
//        }
124
func (fea *Feature) DecodeGeoJSON(bytes []byte, props any) error {
1✔
125
        return fdecode(bytes, fea, props)
1✔
126
}
1✔
127

128
// Decodes GeoJSON object
NEW
129
func UnmarshalJSON[T IFeature](bytes []byte, obj T) error {
×
NEW
130
        return fdecode(bytes, obj, obj)
×
NEW
131
}
×
132

133
func fdecode(bytes []byte, fe IFeature, obj any) error {
1✔
134
        var fea struct {
1✔
135
                Type       string          `json:"type"`
1✔
136
                ID         string          `json:"id,omitempty"`
1✔
137
                Geometry   json.RawMessage `json:"geometry"`
1✔
138
                Properties json.RawMessage `json:"properties,omitempty"`
1✔
139
        }
1✔
140

1✔
141
        if err := json.Unmarshal(bytes, &fea); err != nil {
1✔
142
                return err
×
143
        }
×
144

145
        if fea.Type != TYPE_FEATURE {
2✔
146
                return ErrUnsupportedType
1✔
147
        }
1✔
148

149
        if fea.Properties != nil {
2✔
150
                if err := json.Unmarshal(fea.Properties, &obj); err != nil {
1✔
151
                        return err
×
152
                }
×
153
        }
154

155
        if fea.Geometry != nil {
2✔
156
                geo, err := decodeGeometry(fea.Geometry)
1✔
157
                if err != nil {
1✔
158
                        return err
×
159
                }
×
160
                fe.apply(geo)
1✔
161
        }
162

163
        return nil
1✔
164
}
165

166
// New Feature from Geometry
167
func New(geometry Geometry) Feature {
1✔
168
        return Feature{Geometry: geometry}
1✔
169
}
1✔
170

171
// NewPoint ⟼ Feature[Point]
172
func NewPoint(coords Coord) Feature {
1✔
173
        return Feature{
1✔
174
                Geometry: &Point{Coords: coords},
1✔
175
        }
1✔
176
}
1✔
177

178
// NewMultiPoint ⟼ Feature[MultiPoint]
179
func NewMultiPoint(coords Curve) Feature {
1✔
180
        return Feature{
1✔
181
                Geometry: &MultiPoint{Coords: coords},
1✔
182
        }
1✔
183
}
1✔
184

185
// NewLineString ⟼ Feature[LineString]
186
func NewLineString(coords Curve) Feature {
1✔
187
        return Feature{
1✔
188
                Geometry: &LineString{Coords: coords},
1✔
189
        }
1✔
190
}
1✔
191

192
// NewMultiLineString ⟼ Feature[MultiLineString]
193
func NewMultiLineString(coords Surface) Feature {
1✔
194
        return Feature{
1✔
195
                Geometry: &MultiLineString{Coords: coords},
1✔
196
        }
1✔
197
}
1✔
198

199
// NewPolygon ⟼ Feature[Polygon]
200
func NewPolygon(coords Surface) Feature {
1✔
201
        return Feature{
1✔
202
                Geometry: &Polygon{Coords: coords},
1✔
203
        }
1✔
204
}
1✔
205

206
// NewMultiPolygon ⟼ Feature[MultiPolygon]
207
func NewMultiPolygon(coords ...Surface) Feature {
1✔
208
        return Feature{
1✔
209
                Geometry: &MultiPolygon{Coords: coords},
1✔
210
        }
1✔
211
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc