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

brick / geo / 17456208570

04 Sep 2025 07:10AM UTC coverage: 50.432%. Remained the same
17456208570

push

github

BenMorel
Use @extends and @implements instead of @template-* variants

For consistency with the rest of the project.

1867 of 3702 relevant lines covered (50.43%)

1140.21 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
use function array_map;
16
use function array_merge;
17
use function in_array;
18
use function json_encode;
19

20
use const JSON_PRETTY_PRINT;
21
use const JSON_THROW_ON_ERROR;
22

23
/**
24
 * Converter class from Geometry to GeoJSON.
25
 */
26
final class GeoJsonWriter
27
{
28
    private readonly bool $prettyPrint;
29

30
    private readonly bool $setBbox;
31

32
    private readonly bool $lenient;
33

34
    /**
35
     * @param bool $prettyPrint Whether to pretty-print the JSON output.
36
     * @param bool $setBbox     Whether to set the bbox attribute of each non-empty GeoJSON object.
37
     * @param bool $lenient     Whether to allow nested GeometryCollections, forbidden by the GeoJSON spec.
38
     */
39
    public function __construct(bool $prettyPrint = false, bool $setBbox = false, bool $lenient = false)
40
    {
41
        $this->prettyPrint = $prettyPrint;
456✔
42
        $this->setBbox = $setBbox;
456✔
43
        $this->lenient = $lenient;
456✔
44
    }
45

46
    /**
47
     * Writes the given object as GeoJSON.
48
     *
49
     * @param Geometry|Feature|FeatureCollection $object The object to export as GeoJSON.
50
     *
51
     * @return string The GeoJSON representation of the given object.
52
     *
53
     * @throws GeometryIoException If the given geometry cannot be exported as GeoJSON.
54
     */
55
    public function write(Geometry|Feature|FeatureCollection $object): string
56
    {
57
        $flags = JSON_THROW_ON_ERROR;
456✔
58

59
        if ($this->prettyPrint) {
456✔
60
            $flags |= JSON_PRETTY_PRINT;
40✔
61
        }
62

63
        return json_encode($this->writeRaw($object), $flags);
456✔
64
    }
65

66
    /**
67
     * Writes the given object as a raw stdClass object that can be JSON-encoded.
68
     *
69
     * @return stdClass An object to be JSON-encoded.
70
     *
71
     * @throws GeometryIoException
72
     */
73
    public function writeRaw(Geometry|Feature|FeatureCollection $object): stdClass
74
    {
75
        if ($object instanceof Feature) {
456✔
76
            return $this->writeFeature($object);
192✔
77
        }
78

79
        if ($object instanceof FeatureCollection) {
264✔
80
            return $this->writeFeatureCollection($object);
16✔
81
        }
82

83
        return $this->writeGeometry($object);
248✔
84
    }
85

86
    /**
87
     * @see https://github.com/vimeo/psalm/issues/8187
88
     *
89
     * @throws GeometryIoException
90
     *
91
     * @psalm-suppress MoreSpecificReturnType
92
     * @psalm-suppress LessSpecificReturnStatement
93
     */
94
    private function writeFeature(Feature $feature): stdClass
95
    {
96
        $boundingBox = null;
208✔
97
        $geometry = $feature->getGeometry();
208✔
98

99
        if ($geometry !== null) {
208✔
100
            if ($this->setBbox) {
192✔
101
                $boundingBox = $geometry->getBoundingBox();
×
102
            }
103

104
            $geometry = $this->writeGeometry($geometry);
192✔
105
        }
106

107
        $result = [
208✔
108
            'type' => 'Feature',
208✔
109
            'properties' => $feature->getProperties(),
208✔
110
            'geometry' => $geometry,
208✔
111
        ];
208✔
112

113
        if ($boundingBox !== null && ! $boundingBox->isEmpty()) {
208✔
114
            $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
×
115
        }
116

117
        return (object) $result;
208✔
118
    }
119

120
    /**
121
     * @see https://github.com/vimeo/psalm/issues/8187
122
     *
123
     * @throws GeometryIoException
124
     *
125
     * @psalm-suppress MoreSpecificReturnType
126
     * @psalm-suppress LessSpecificReturnStatement
127
     */
128
    private function writeFeatureCollection(FeatureCollection $featureCollection): stdClass
129
    {
130
        $features = $featureCollection->getFeatures();
16✔
131
        $features = array_map(fn (Feature $feature) => $this->writeFeature($feature), $features);
16✔
132

133
        $result = [
16✔
134
            'type' => 'FeatureCollection',
16✔
135
            'features' => $features,
16✔
136
        ];
16✔
137

138
        if ($this->setBbox) {
16✔
139
            $boundingBox = BoundingBox::new();
×
140

141
            foreach ($featureCollection->getFeatures() as $feature) {
×
142
                $featureGeometry = $feature->getGeometry();
×
143

144
                if ($featureGeometry !== null) {
×
145
                    $boundingBox = $boundingBox->extendedWithBoundingBox($featureGeometry->getBoundingBox());
×
146
                }
147
            }
148

149
            if (! $boundingBox->isEmpty()) {
×
150
                $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
×
151
            }
152
        }
153

154
        return (object) $result;
16✔
155
    }
156

157
    /**
158
     * @see https://github.com/vimeo/psalm/issues/8187
159
     *
160
     * @throws GeometryIoException
161
     *
162
     * @psalm-suppress MoreSpecificReturnType
163
     * @psalm-suppress LessSpecificReturnStatement
164
     */
165
    private function writeGeometry(Geometry $geometry): stdClass
166
    {
167
        // GeoJSON supports XY & XYZ only
168
        $geometry = $geometry->withoutM();
440✔
169

170
        // filter out MultiPoint, MultiLineString and MultiPolygon
171
        if ($geometry instanceof GeometryCollection && $geometry->geometryType() === 'GeometryCollection') {
440✔
172
            return $this->writeGeometryCollection($geometry);
88✔
173
        }
174

175
        $geometryType = $geometry->geometryType();
416✔
176

177
        $validGeometries = [
416✔
178
            'Point',
416✔
179
            'LineString',
416✔
180
            'Polygon',
416✔
181
            'MultiPoint',
416✔
182
            'MultiLineString',
416✔
183
            'MultiPolygon',
416✔
184
        ];
416✔
185

186
        if (! in_array($geometryType, $validGeometries, true)) {
416✔
187
            throw GeometryIoException::unsupportedGeometryType($geometry->geometryType());
×
188
        }
189

190
        $result = [
416✔
191
            'type' => $geometryType,
416✔
192
            'coordinates' => $geometry->toArray(),
416✔
193
        ];
416✔
194

195
        if ($this->setBbox) {
416✔
196
            $boundingBox = $geometry->getBoundingBox();
8✔
197

198
            if (! $boundingBox->isEmpty()) {
8✔
199
                $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
8✔
200
            }
201
        }
202

203
        return (object) $result;
416✔
204
    }
205

206
    /**
207
     * @see https://github.com/vimeo/psalm/issues/8187
208
     *
209
     * @throws GeometryIoException
210
     *
211
     * @psalm-suppress MoreSpecificReturnType
212
     * @psalm-suppress LessSpecificReturnStatement
213
     */
214
    private function writeGeometryCollection(GeometryCollection $geometryCollection): stdClass
215
    {
216
        $geometries = $geometryCollection->geometries();
88✔
217

218
        $geometries = array_map(function (Geometry $geometry) {
88✔
219
            if ($geometry::class === GeometryCollection::class && ! $this->lenient) {
72✔
220
                throw new GeometryIoException(
8✔
221
                    'GeoJSON does not allow nested GeometryCollections. ' .
8✔
222
                    'You can allow this by setting the $lenient flag to true.',
8✔
223
                );
8✔
224
            }
225

226
            return $this->writeGeometry($geometry);
64✔
227
        }, $geometries);
88✔
228

229
        $result = [
80✔
230
            'type' => 'GeometryCollection',
80✔
231
            'geometries' => $geometries,
80✔
232
        ];
80✔
233

234
        if ($this->setBbox) {
80✔
235
            $boundingBox = $geometryCollection->getBoundingBox();
×
236

237
            if (! $boundingBox->isEmpty()) {
×
238
                $result['bbox'] = $this->bboxToCoordinateArray($boundingBox);
×
239
            }
240
        }
241

242
        return (object) $result;
80✔
243
    }
244

245
    private function bboxToCoordinateArray(BoundingBox $boundingBox): array
246
    {
247
        return array_merge(
8✔
248
            $boundingBox->getSouthWest()->toArray(),
8✔
249
            $boundingBox->getNorthEast()->toArray(),
8✔
250
        );
8✔
251
    }
252
}
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