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

brick / geo / 13872073621

15 Mar 2025 10:37AM UTC coverage: 51.165% (-36.2%) from 87.322%
13872073621

push

github

BenMorel
Add support for GeosOp in requireEngine()

1866 of 3647 relevant lines covered (51.17%)

1154.34 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;
456✔
34
        $this->setBbox = $setBbox;
456✔
35
        $this->lenient = $lenient;
456✔
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;
456✔
50

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

55
        return json_encode($this->writeRaw($object), $flags);
456✔
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) {
456✔
70
            return $this->writeFeature($object);
192✔
71
        }
72

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

77
        return $this->writeGeometry($object);
248✔
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;
208✔
90
        $geometry = $feature->getGeometry();
208✔
91

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

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

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

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

110
        return (object) $result;
208✔
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();
16✔
123
        $features = array_map(fn(Feature $feature) => $this->writeFeature($feature), $features);
16✔
124

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

130
        if ($this->setBbox) {
16✔
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;
16✔
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();
440✔
160

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

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

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

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

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

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

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

194
        return (object) $result;
416✔
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();
88✔
207

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

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

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

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

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

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

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