Skip to content

Commit 3cec77e

Browse files
feat(parser): parse LineStyle, PolyStyle and SimpleData
getStyles() only read IconStyle and LabelStyle, so a LineString or a Polygon came back with no stroke, width or fill and could not be drawn from the parsed data alone. LineStyle and PolyStyle are now parsed too. Only the elements a style actually declares are reported. KML defines defaults for all of them, but filling those in here would stop the caller telling "the file said nothing" apart from "the file said exactly the default". `fill` and `outline` are read with isset() rather than a truthiness check, so an explicit <fill>0</fill> is reported as false instead of disappearing. ExtendedData only read <Data> pairs and ignored <SchemaData> entirely, which is how ogr2ogr and QGIS emit attributes: as <SimpleData> inside a <SchemaData> block. Those exports came back with no extended data at all. Both forms now land in the same map, SimpleData applied last so an explicit schema value wins over a plain Data pair of the same name.
1 parent 2c37af3 commit 3cec77e

3 files changed

Lines changed: 273 additions & 7 deletions

File tree

src/KmlParser.php

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,7 @@ public function getPlacemarks(): array
117117
}
118118

119119
if ($placemarkXml->ExtendedData) {
120-
$extendedData = [];
121-
foreach ($placemarkXml->ExtendedData->Data as $data) {
122-
$name = (string) $data->attributes()->name;
123-
$value = (string) $data->value;
124-
$extendedData[$name] = $value;
125-
}
126-
$placemark['extendedData'] = $extendedData;
120+
$placemark['extendedData'] = $this->parseExtendedData($placemarkXml->ExtendedData);
127121
}
128122

129123
$placemarks[] = $placemark;
@@ -247,12 +241,96 @@ public function getStyles(): array
247241
}
248242
}
249243

244+
if ($styleXml->LineStyle) {
245+
$style['lineStyle'] = $this->parseLineStyle($styleXml->LineStyle);
246+
}
247+
248+
if ($styleXml->PolyStyle) {
249+
$style['polyStyle'] = $this->parsePolyStyle($styleXml->PolyStyle);
250+
}
251+
250252
$styles[$id] = $style;
251253
}
252254

253255
return $styles;
254256
}
255257

258+
/**
259+
* KML carries typed attributes two different ways: <Data> pairs, and
260+
* <SimpleData> entries inside a <SchemaData> block, which is what ogr2ogr
261+
* and QGIS emit. Both land in the same map, SimpleData last so an explicit
262+
* schema value wins over a plain Data pair of the same name.
263+
*
264+
* @return array<string, string>
265+
*/
266+
protected function parseExtendedData(SimpleXMLElement $extendedData): array
267+
{
268+
$parsed = [];
269+
270+
foreach ($extendedData->Data as $data) {
271+
$parsed[(string) $data->attributes()->name] = (string) $data->value;
272+
}
273+
274+
foreach ($extendedData->SchemaData as $schemaData) {
275+
foreach ($schemaData->SimpleData as $simpleData) {
276+
$parsed[(string) $simpleData->attributes()->name] = (string) $simpleData;
277+
}
278+
}
279+
280+
return $parsed;
281+
}
282+
283+
/**
284+
* The stroke of a LineString and the outline of a Polygon.
285+
*
286+
* Only the elements the document actually declares are reported. KML
287+
* defines defaults for both, but filling them in here would stop the
288+
* caller from telling "the file said nothing" apart from "the file said
289+
* exactly the default".
290+
*
291+
* @return array<string, string|float>
292+
*/
293+
protected function parseLineStyle(SimpleXMLElement $lineStyle): array
294+
{
295+
$parsed = [];
296+
297+
if (isset($lineStyle->color)) {
298+
$parsed['color'] = (string) $lineStyle->color;
299+
}
300+
301+
if (isset($lineStyle->width)) {
302+
$parsed['width'] = (float) $lineStyle->width;
303+
}
304+
305+
return $parsed;
306+
}
307+
308+
/**
309+
* The fill of a Polygon. `fill` and `outline` are the KML booleans 0 and 1,
310+
* and are read with isset() rather than a truthiness check so that an
311+
* explicit <fill>0</fill> is reported instead of being dropped.
312+
*
313+
* @return array<string, string|bool>
314+
*/
315+
protected function parsePolyStyle(SimpleXMLElement $polyStyle): array
316+
{
317+
$parsed = [];
318+
319+
if (isset($polyStyle->color)) {
320+
$parsed['color'] = (string) $polyStyle->color;
321+
}
322+
323+
if (isset($polyStyle->fill)) {
324+
$parsed['fill'] = (string) $polyStyle->fill === '1';
325+
}
326+
327+
if (isset($polyStyle->outline)) {
328+
$parsed['outline'] = (string) $polyStyle->outline === '1';
329+
}
330+
331+
return $parsed;
332+
}
333+
256334
/**
257335
* Get StyleMap Node from the KML
258336
*

tests/ExtendedDataTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
use PlinCode\KmlParser\KmlParser;
4+
5+
function kmlWithExtendedData(string $extendedData): string
6+
{
7+
return <<<XML
8+
<?xml version="1.0" encoding="UTF-8"?>
9+
<kml xmlns="http://www.opengis.net/kml/2.2">
10+
<Document>
11+
<Placemark>
12+
<name>Sample</name>
13+
<ExtendedData>
14+
{$extendedData}
15+
</ExtendedData>
16+
<Point>
17+
<coordinates>7.1,45.1,0</coordinates>
18+
</Point>
19+
</Placemark>
20+
</Document>
21+
</kml>
22+
XML;
23+
}
24+
25+
it('parses Data pairs', function () {
26+
$kml = kmlWithExtendedData(<<<'XML'
27+
<Data name="area"><value>12</value></Data>
28+
<Data name="region"><value>Piemonte</value></Data>
29+
XML);
30+
31+
expect((new KmlParser)->loadFromString($kml)->getPlacemarks()[0]['extendedData'])
32+
->toBe(['area' => '12', 'region' => 'Piemonte']);
33+
});
34+
35+
it('parses SimpleData entries inside a SchemaData block', function () {
36+
$kml = kmlWithExtendedData(<<<'XML'
37+
<SchemaData schemaUrl="#sample">
38+
<SimpleData name="area">12</SimpleData>
39+
<SimpleData name="region">Piemonte</SimpleData>
40+
</SchemaData>
41+
XML);
42+
43+
expect((new KmlParser)->loadFromString($kml)->getPlacemarks()[0]['extendedData'])
44+
->toBe(['area' => '12', 'region' => 'Piemonte']);
45+
});
46+
47+
it('merges Data pairs and SimpleData entries', function () {
48+
$kml = kmlWithExtendedData(<<<'XML'
49+
<Data name="area"><value>12</value></Data>
50+
<SchemaData schemaUrl="#sample">
51+
<SimpleData name="region">Piemonte</SimpleData>
52+
</SchemaData>
53+
XML);
54+
55+
expect((new KmlParser)->loadFromString($kml)->getPlacemarks()[0]['extendedData'])
56+
->toBe(['area' => '12', 'region' => 'Piemonte']);
57+
});
58+
59+
it('reads SimpleData across several SchemaData blocks', function () {
60+
$kml = kmlWithExtendedData(<<<'XML'
61+
<SchemaData schemaUrl="#one">
62+
<SimpleData name="area">12</SimpleData>
63+
</SchemaData>
64+
<SchemaData schemaUrl="#two">
65+
<SimpleData name="region">Piemonte</SimpleData>
66+
</SchemaData>
67+
XML);
68+
69+
expect((new KmlParser)->loadFromString($kml)->getPlacemarks()[0]['extendedData'])
70+
->toBe(['area' => '12', 'region' => 'Piemonte']);
71+
});
72+
73+
it('carries extended data through to GeoJSON properties', function () {
74+
$kml = kmlWithExtendedData(<<<'XML'
75+
<SchemaData schemaUrl="#sample">
76+
<SimpleData name="area">12</SimpleData>
77+
</SchemaData>
78+
XML);
79+
80+
expect((new KmlParser)->loadFromString($kml)->toGeoJson()['features'][0]['properties'])
81+
->toBe([
82+
'name' => 'Sample',
83+
'description' => '',
84+
'extendedData' => ['area' => '12'],
85+
]);
86+
});

tests/StyleDetailsTest.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
use PlinCode\KmlParser\KmlParser;
4+
5+
$styledKml = <<<'XML'
6+
<?xml version="1.0" encoding="UTF-8"?>
7+
<kml xmlns="http://www.opengis.net/kml/2.2">
8+
<Document>
9+
<Style id="route">
10+
<LineStyle>
11+
<color>ff0000ff</color>
12+
<width>4</width>
13+
</LineStyle>
14+
</Style>
15+
<Style id="area">
16+
<LineStyle>
17+
<color>ff00ff00</color>
18+
</LineStyle>
19+
<PolyStyle>
20+
<color>7f00ff00</color>
21+
<fill>0</fill>
22+
<outline>1</outline>
23+
</PolyStyle>
24+
</Style>
25+
<Placemark>
26+
<name>Line</name>
27+
<styleUrl>#route</styleUrl>
28+
<LineString>
29+
<coordinates>7.1,45.1,0 7.2,45.2,0</coordinates>
30+
</LineString>
31+
</Placemark>
32+
</Document>
33+
</kml>
34+
XML;
35+
36+
it('parses the stroke declared by a LineStyle', function () use ($styledKml) {
37+
$style = (new KmlParser)->loadFromString($styledKml)->getStyles()['route'];
38+
39+
expect($style['lineStyle'])->toBe([
40+
'color' => 'ff0000ff',
41+
'width' => 4.0,
42+
]);
43+
});
44+
45+
it('parses the fill declared by a PolyStyle', function () use ($styledKml) {
46+
$style = (new KmlParser)->loadFromString($styledKml)->getStyles()['area'];
47+
48+
expect($style['polyStyle'])->toBe([
49+
'color' => '7f00ff00',
50+
'fill' => false,
51+
'outline' => true,
52+
]);
53+
});
54+
55+
it('reports an explicit fill of zero instead of dropping it', function () use ($styledKml) {
56+
$style = (new KmlParser)->loadFromString($styledKml)->getStyles()['area'];
57+
58+
expect($style['polyStyle'])->toHaveKey('fill')
59+
->and($style['polyStyle']['fill'])->toBeFalse();
60+
});
61+
62+
it('omits the elements a style does not declare', function () use ($styledKml) {
63+
$styles = (new KmlParser)->loadFromString($styledKml)->getStyles();
64+
65+
expect($styles['route'])->not->toHaveKey('polyStyle')
66+
->and($styles['route'])->not->toHaveKey('iconStyle')
67+
->and($styles['route'])->not->toHaveKey('labelStyle')
68+
->and($styles['area']['lineStyle'])->toBe(['color' => 'ff00ff00'])
69+
->and($styles['area']['lineStyle'])->not->toHaveKey('width');
70+
});
71+
72+
it('keeps parsing IconStyle and LabelStyle alongside the new ones', function () {
73+
$kml = <<<'XML'
74+
<?xml version="1.0" encoding="UTF-8"?>
75+
<kml xmlns="http://www.opengis.net/kml/2.2">
76+
<Document>
77+
<Style id="pin">
78+
<IconStyle>
79+
<scale>1.4</scale>
80+
<Icon><href>images/icon-1.png</href></Icon>
81+
</IconStyle>
82+
<LabelStyle>
83+
<scale>0.5</scale>
84+
<color>ff112233</color>
85+
</LabelStyle>
86+
<LineStyle>
87+
<width>2</width>
88+
</LineStyle>
89+
</Style>
90+
<Placemark>
91+
<Point><coordinates>7.1,45.1,0</coordinates></Point>
92+
</Placemark>
93+
</Document>
94+
</kml>
95+
XML;
96+
97+
$style = (new KmlParser)->loadFromString($kml)->getStyles()['pin'];
98+
99+
expect($style['iconStyle'])->toBe(['scale' => 1.4, 'href' => 'images/icon-1.png'])
100+
->and($style['labelStyle'])->toBe(['scale' => 0.5, 'color' => 'ff112233'])
101+
->and($style['lineStyle'])->toBe(['width' => 2.0]);
102+
});

0 commit comments

Comments
 (0)