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

brick / geo / 13753277563

09 Mar 2025 10:43PM UTC coverage: 49.787% (+2.5%) from 47.295%
13753277563

push

github

BenMorel
Prepare for release

1749 of 3513 relevant lines covered (49.79%)

975.53 hits per line

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

84.88
/src/IO/GeoJSONWriter.php
1
<?php
2

3
declare(strict_types = 1);
4

5
namespace Brick\Geo\IO;
6

7
use Brick\Geo\BoundingBox;
8
use Brick\Geo\Exception\GeometryIOException;
9
use Brick\Geo\Geometry;
10
use Brick\Geo\GeometryCollection;
11
use Brick\Geo\IO\GeoJSON\Feature;
12
use Brick\Geo\IO\GeoJSON\FeatureCollection;
13
use stdClass;
14

15
/**
16
 * Converter class from Geometry to GeoJSON.
17
 */
18
final class GeoJSONWriter
19
{
20
    private readonly bool $prettyPrint;
21

22
    private readonly bool $setBbox;
23

24
    private readonly bool $lenient;
25

26
    /**
27
     * @param bool $prettyPrint Whether to pretty-print the JSON output.
28
     * @param bool $setBbox     Whether to set the bbox attribute of each non-empty GeoJSON object.
29
     * @param bool $lenient     Whether to allow nested GeometryCollections, forbidden by the GeoJSON spec.
30
     */
31
    public function __construct(bool $prettyPrint = false, bool $setBbox = false, bool $lenient = false)
32
    {
33
        $this->prettyPrint = $prettyPrint;
399✔
34
        $this->setBbox = $setBbox;
399✔
35
        $this->lenient = $lenient;
399✔
36
    }
37

38
    /**
39
     * Writes the given object as GeoJSON.
40
     *
41
     * @param Geometry|Feature|FeatureCollection $object The object to export as GeoJSON.
42
     *
43
     * @return string The GeoJSON representation of the given object.
44
     *
45
     * @throws GeometryIOException If the given geometry cannot be exported as GeoJSON.
46
     */
47
    public function write(Geometry|Feature|FeatureCollection $object) : string
48
    {
49
        $flags = JSON_THROW_ON_ERROR;
399✔
50

51
        if ($this->prettyPrint) {
399✔
52
            $flags |= JSON_PRETTY_PRINT;
35✔
53
        }
54

55
        return json_encode($this->writeRaw($object), $flags);
399✔
56
    }
57

58
    /**
59
     * Writes the given object as a raw stdClass object that can be JSON-encoded.
60
     *
61
     * @param Geometry|Feature|FeatureCollection $object
62
     *
63
     * @return stdClass An object to be JSON-encoded.
64
     *
65
     * @throws GeometryIOException
66
     */
67
    public function writeRaw(Geometry|Feature|FeatureCollection $object): stdClass
68
    {
69
        if ($object instanceof Feature) {
399✔
70
            return $this->writeFeature($object);
168✔
71
        }
72

73
        if ($object instanceof FeatureCollection) {
231✔
74
            return $this->writeFeatureCollection($object);
14✔
75
        }
76

77
        return $this->writeGeometry($object);
217✔
78
    }
79

80
    /**
81
     * @psalm-suppress MoreSpecificReturnType
82
     * @psalm-suppress LessSpecificReturnStatement
83
     * @see https://github.com/vimeo/psalm/issues/8187
84
     *
85
     * @throws GeometryIOException
86
     */
87
    private function writeFeature(Feature $feature): stdClass
88
    {
89
        $boundingBox = null;
182✔
90
        $geometry = $feature->getGeometry();
182✔
91

92
        if ($geometry !== null) {
182✔
93
            if ($this->setBbox) {
168✔
94
                $boundingBox = $geometry->getBoundingBox();
×
95
            }
96

97
            $geometry = $this->writeGeometry($geometry);
168✔
98
        }
99

100
        $result = [
182✔
101
            'type' => 'Feature',
182✔
102
            'properties' => $feature->getProperties(),
182✔
103
            'geometry' => $geometry
182✔
104
        ];
182✔
105

106
        if ($boundingBox !== null && ! $boundingBox->isEmpty()) {
182✔
107
            $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
×
108
        }
109

110
        return (object) $result;
182✔
111
    }
112

113
    /**
114
     * @psalm-suppress MoreSpecificReturnType
115
     * @psalm-suppress LessSpecificReturnStatement
116
     * @see https://github.com/vimeo/psalm/issues/8187
117
     *
118
     * @throws GeometryIOException
119
     */
120
    private function writeFeatureCollection(FeatureCollection $featureCollection): stdClass
121
    {
122
        $features = $featureCollection->getFeatures();
14✔
123
        $features = array_map(fn(Feature $feature) => $this->writeFeature($feature), $features);
14✔
124

125
        $result = [
14✔
126
            'type' => 'FeatureCollection',
14✔
127
            'features' => $features
14✔
128
        ];
14✔
129

130
        if ($this->setBbox) {
14✔
131
            $boundingBox = BoundingBox::new();
×
132

133
            foreach ($featureCollection->getFeatures() as $feature) {
×
134
                $featureGeometry = $feature->getGeometry();
×
135

136
                if ($featureGeometry !== null) {
×
137
                    $boundingBox = $boundingBox->extendedWithBoundingBox($featureGeometry->getBoundingBox());
×
138
                }
139
            }
140

141
            if (! $boundingBox->isEmpty()) {
×
142
                $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
×
143
            }
144
        }
145

146
        return (object) $result;
14✔
147
    }
148

149
    /**
150
     * @psalm-suppress MoreSpecificReturnType
151
     * @psalm-suppress LessSpecificReturnStatement
152
     * @see https://github.com/vimeo/psalm/issues/8187
153
     *
154
     * @throws GeometryIOException
155
     */
156
    private function writeGeometry(Geometry $geometry): stdClass
157
    {
158
        // GeoJSON supports XY & XYZ only
159
        $geometry = $geometry->withoutM();
385✔
160

161
        // filter out MultiPoint, MultiLineString and MultiPolygon
162
        if ($geometry instanceof GeometryCollection && $geometry->geometryType() === 'GeometryCollection') {
385✔
163
            return $this->writeGeometryCollection($geometry);
77✔
164
        }
165

166
        $geometryType = $geometry->geometryType();
364✔
167

168
        $validGeometries = [
364✔
169
            'Point',
364✔
170
            'LineString',
364✔
171
            'Polygon',
364✔
172
            'MultiPoint',
364✔
173
            'MultiLineString',
364✔
174
            'MultiPolygon'
364✔
175
        ];
364✔
176

177
        if (! in_array($geometryType, $validGeometries, true)) {
364✔
178
            throw GeometryIOException::unsupportedGeometryType($geometry->geometryType());
×
179
        }
180

181
        $result = [
364✔
182
            'type' => $geometryType,
364✔
183
            'coordinates' => $geometry->toArray()
364✔
184
        ];
364✔
185

186
        if ($this->setBbox) {
364✔
187
            $boundingBox = $geometry->getBoundingBox();
7✔
188

189
            if (! $boundingBox->isEmpty()) {
7✔
190
                $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
7✔
191
            }
192
        }
193

194
        return (object) $result;
364✔
195
    }
196

197
    /**
198
     * @psalm-suppress MoreSpecificReturnType
199
     * @psalm-suppress LessSpecificReturnStatement
200
     * @see https://github.com/vimeo/psalm/issues/8187
201
     *
202
     * @throws GeometryIOException
203
     */
204
    private function writeGeometryCollection(GeometryCollection $geometryCollection): stdClass
205
    {
206
        $geometries = $geometryCollection->geometries();
77✔
207

208
        $geometries = array_map(function(Geometry $geometry) {
77✔
209
            if ($geometry::class === GeometryCollection::class && ! $this->lenient) {
63✔
210
                throw new GeometryIOException(
7✔
211
                    'GeoJSON does not allow nested GeometryCollections. ' .
7✔
212
                    'You can allow this by setting the $lenient flag to true.',
7✔
213
                );
7✔
214
            }
215

216
            return $this->writeGeometry($geometry);
56✔
217
        }, $geometries);
77✔
218

219
        $result = [
70✔
220
            'type' => 'GeometryCollection',
70✔
221
            'geometries' => $geometries
70✔
222
        ];
70✔
223

224
        if ($this->setBbox) {
70✔
225
            $boundingBox = $geometryCollection->getBoundingBox();
×
226

227
            if (! $boundingBox->isEmpty()) {
×
228
                $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
×
229
            }
230
        }
231

232
        return (object) $result;
70✔
233
    }
234

235
    private function bboxToCoordinateArray(BoundingBox $boundingBox): array
236
    {
237
        return array_merge(
7✔
238
            $boundingBox->getSouthWest()->toArray(),
7✔
239
            $boundingBox->getNorthEast()->toArray()
7✔
240
        );
7✔
241
    }
242
}
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

© 2026 Coveralls, Inc