33namespace PlinCode \KmlParser ;
44
55use Exception ;
6+ use PlinCode \KmlParser \Enums \GeometryType ;
67use PlinCode \KmlParser \Exceptions \KmlParserException ;
78use PlinCode \KmlParser \Traits \ParsesCoordinates ;
89use PlinCode \KmlParser \Validators \KmlValidator ;
@@ -99,26 +100,16 @@ public function getPlacemarks(): array
99100 'description ' => (string ) $ placemarkXml ->description ,
100101 ];
101102
102- if ($ placemarkXml ->Point ) {
103- $ coords = (string ) $ placemarkXml ->Point ->coordinates ;
104- $ coordsArray = explode (', ' , trim ($ coords ));
105- $ placemark ['type ' ] = 'Point ' ;
106- $ placemark ['coordinates ' ] = [
107- 'longitude ' => (float ) $ coordsArray [0 ],
108- 'latitude ' => (float ) $ coordsArray [1 ],
109- 'altitude ' => isset ($ coordsArray [2 ]) ? (float ) $ coordsArray [2 ] : 0 ,
110- ];
111- }
103+ foreach (GeometryType::cases () as $ type ) {
104+ if ($ placemarkXml ->{$ type ->value }) {
105+ $ geometry = $ this ->parseGeometry ($ type , $ placemarkXml ->{$ type ->value });
112106
113- if ($ placemarkXml ->LineString ) {
114- $ coords = (string ) $ placemarkXml ->LineString ->coordinates ;
115- $ placemark ['type ' ] = 'LineString ' ;
116- $ placemark ['coordinates ' ] = $ this ->parseLineStringCoordinates ($ coords );
117- }
107+ if ($ geometry !== null ) {
108+ $ placemark = array_merge ($ placemark , $ geometry );
109+ }
118110
119- if ($ placemarkXml ->Polygon ) {
120- $ placemark ['type ' ] = 'Polygon ' ;
121- $ placemark ['coordinates ' ] = $ this ->parsePolygonCoordinates ($ placemarkXml ->Polygon );
111+ break ;
112+ }
122113 }
123114
124115 if ($ placemarkXml ->styleUrl ) {
@@ -141,6 +132,60 @@ public function getPlacemarks(): array
141132 return $ placemarks ;
142133 }
143134
135+ /**
136+ * Turn one KML geometry element into its array representation.
137+ *
138+ * @return array<string, mixed>|null
139+ */
140+ protected function parseGeometry (GeometryType $ type , SimpleXMLElement $ geometry ): ?array
141+ {
142+ return match ($ type ) {
143+ GeometryType::POINT => [
144+ 'type ' => $ type ->value ,
145+ 'coordinates ' => $ this ->parsePointCoordinates ((string ) $ geometry ->coordinates ),
146+ ],
147+ GeometryType::LINE_STRING => [
148+ 'type ' => $ type ->value ,
149+ 'coordinates ' => $ this ->parseLineStringCoordinates ((string ) $ geometry ->coordinates ),
150+ ],
151+ GeometryType::POLYGON => [
152+ 'type ' => $ type ->value ,
153+ 'coordinates ' => $ this ->parsePolygonCoordinates ($ geometry ),
154+ ],
155+ GeometryType::MULTI_GEOMETRY => [
156+ 'type ' => $ type ->value ,
157+ 'geometries ' => $ this ->parseMultiGeometry ($ geometry ),
158+ ],
159+ };
160+ }
161+
162+ /**
163+ * A MultiGeometry holds nested geometries instead of coordinates, and KML
164+ * allows those to be MultiGeometry elements in turn.
165+ *
166+ * @return array<int, array<string, mixed>>
167+ */
168+ protected function parseMultiGeometry (SimpleXMLElement $ multiGeometry ): array
169+ {
170+ $ geometries = [];
171+
172+ foreach ($ multiGeometry ->children () as $ name => $ child ) {
173+ $ type = GeometryType::tryFrom ((string ) $ name );
174+
175+ if ($ type === null ) {
176+ continue ;
177+ }
178+
179+ $ geometry = $ this ->parseGeometry ($ type , $ child );
180+
181+ if ($ geometry !== null ) {
182+ $ geometries [] = $ geometry ;
183+ }
184+ }
185+
186+ return $ geometries ;
187+ }
188+
144189 /**
145190 * Get Style Node from the KML
146191 *
@@ -250,86 +295,32 @@ public function getStyleMaps(): array
250295 public function toGeoJson (): array
251296 {
252297 $ features = [];
253- $ placemarks = $ this ->getPlacemarks ();
254-
255- foreach ($ placemarks as $ placemark ) {
256- if (isset ($ placemark ['coordinates ' ])) {
257- $ feature = [
258- 'type ' => 'Feature ' ,
259- 'properties ' => [
260- 'name ' => $ placemark ['name ' ],
261- 'description ' => $ placemark ['description ' ],
262- ],
263- ];
264-
265- // Set geometry based on type
266- if ($ placemark ['type ' ] === 'Point ' ) {
267- $ feature ['geometry ' ] = [
268- 'type ' => 'Point ' ,
269- 'coordinates ' => [
270- $ placemark ['coordinates ' ]['longitude ' ],
271- $ placemark ['coordinates ' ]['latitude ' ],
272- $ placemark ['coordinates ' ]['altitude ' ],
273- ],
274- ];
275- } elseif ($ placemark ['type ' ] === 'LineString ' ) {
276- $ coordinates = [];
277- foreach ($ placemark ['coordinates ' ] as $ coord ) {
278- $ coordinates [] = [
279- $ coord ['longitude ' ],
280- $ coord ['latitude ' ],
281- $ coord ['altitude ' ],
282- ];
283- }
284-
285- $ feature ['geometry ' ] = [
286- 'type ' => 'LineString ' ,
287- 'coordinates ' => $ coordinates ,
288- ];
289- } elseif ($ placemark ['type ' ] === 'Polygon ' ) {
290- $ outerCoordinates = [];
291- foreach ($ placemark ['coordinates ' ]['outerBoundary ' ] as $ coord ) {
292- $ outerCoordinates [] = [
293- $ coord ['longitude ' ],
294- $ coord ['latitude ' ],
295- $ coord ['altitude ' ],
296- ];
297- }
298298
299- $ innerCoordinates = [];
300- foreach ($ placemark ['coordinates ' ]['innerBoundaries ' ] as $ innerBoundary ) {
301- $ innerBoundaryCoords = [];
302- foreach ($ innerBoundary as $ coord ) {
303- $ innerBoundaryCoords [] = [
304- $ coord ['longitude ' ],
305- $ coord ['latitude ' ],
306- $ coord ['altitude ' ],
307- ];
308- }
309- $ innerCoordinates [] = $ innerBoundaryCoords ;
310- }
311-
312- $ allCoordinates = [$ outerCoordinates ];
313- if (! empty ($ innerCoordinates )) {
314- $ allCoordinates = array_merge ($ allCoordinates , $ innerCoordinates );
315- }
299+ foreach ($ this ->getPlacemarks () as $ placemark ) {
300+ $ geometry = $ this ->toGeoJsonGeometry ($ placemark );
316301
317- $ feature ['geometry ' ] = [
318- 'type ' => 'Polygon ' ,
319- 'coordinates ' => $ allCoordinates ,
320- ];
321- }
302+ if ($ geometry === null ) {
303+ continue ;
304+ }
322305
323- if (isset ($ placemark ['styleUrl ' ])) {
324- $ feature ['properties ' ]['styleUrl ' ] = $ placemark ['styleUrl ' ];
325- }
306+ $ feature = [
307+ 'type ' => 'Feature ' ,
308+ 'properties ' => [
309+ 'name ' => $ placemark ['name ' ],
310+ 'description ' => $ placemark ['description ' ],
311+ ],
312+ 'geometry ' => $ geometry ,
313+ ];
326314
327- if (isset ($ placemark ['extendedData ' ])) {
328- $ feature ['properties ' ]['extendedData ' ] = $ placemark ['extendedData ' ];
329- }
315+ if (isset ($ placemark ['styleUrl ' ])) {
316+ $ feature ['properties ' ]['styleUrl ' ] = $ placemark ['styleUrl ' ];
317+ }
330318
331- $ features [] = $ feature ;
319+ if (isset ($ placemark ['extendedData ' ])) {
320+ $ feature ['properties ' ]['extendedData ' ] = $ placemark ['extendedData ' ];
332321 }
322+
323+ $ features [] = $ feature ;
333324 }
334325
335326 return [
@@ -338,6 +329,76 @@ public function toGeoJson(): array
338329 ];
339330 }
340331
332+ /**
333+ * A KML MultiGeometry maps onto a GeoJSON GeometryCollection, which nests
334+ * the same way, so this recurses alongside parseMultiGeometry().
335+ *
336+ * @param array<string, mixed> $geometry
337+ * @return array<string, mixed>|null
338+ */
339+ protected function toGeoJsonGeometry (array $ geometry ): ?array
340+ {
341+ return match ($ geometry ['type ' ] ?? null ) {
342+ GeometryType::POINT ->value => [
343+ 'type ' => 'Point ' ,
344+ 'coordinates ' => $ this ->toGeoJsonPosition ($ geometry ['coordinates ' ]),
345+ ],
346+ GeometryType::LINE_STRING ->value => [
347+ 'type ' => 'LineString ' ,
348+ 'coordinates ' => array_map (
349+ fn (array $ position ) => $ this ->toGeoJsonPosition ($ position ),
350+ $ geometry ['coordinates ' ],
351+ ),
352+ ],
353+ GeometryType::POLYGON ->value => [
354+ 'type ' => 'Polygon ' ,
355+ 'coordinates ' => $ this ->toGeoJsonRings ($ geometry ['coordinates ' ]),
356+ ],
357+ GeometryType::MULTI_GEOMETRY ->value => [
358+ 'type ' => 'GeometryCollection ' ,
359+ 'geometries ' => array_values (array_filter (array_map (
360+ fn (array $ child ) => $ this ->toGeoJsonGeometry ($ child ),
361+ $ geometry ['geometries ' ],
362+ ))),
363+ ],
364+ default => null ,
365+ };
366+ }
367+
368+ /**
369+ * @param array{longitude: float, latitude: float, altitude: float} $position
370+ * @return array<int, float>
371+ */
372+ protected function toGeoJsonPosition (array $ position ): array
373+ {
374+ return [$ position ['longitude ' ], $ position ['latitude ' ], $ position ['altitude ' ]];
375+ }
376+
377+ /**
378+ * GeoJSON puts the outer ring first and every inner ring after it.
379+ *
380+ * @param array{outerBoundary: array<int, array<string, float>>, innerBoundaries: array<int, array<int, array<string, float>>>} $boundaries
381+ * @return array<int, array<int, array<int, float>>>
382+ */
383+ protected function toGeoJsonRings (array $ boundaries ): array
384+ {
385+ $ rings = [
386+ array_map (
387+ fn (array $ position ) => $ this ->toGeoJsonPosition ($ position ),
388+ $ boundaries ['outerBoundary ' ],
389+ ),
390+ ];
391+
392+ foreach ($ boundaries ['innerBoundaries ' ] as $ innerBoundary ) {
393+ $ rings [] = array_map (
394+ fn (array $ position ) => $ this ->toGeoJsonPosition ($ position ),
395+ $ innerBoundary ,
396+ );
397+ }
398+
399+ return $ rings ;
400+ }
401+
341402 /**
342403 * Get Document Node from the KML
343404 *
0 commit comments