Skip to content

Commit 61bf0e9

Browse files
authored
Merge pull request #1155 from Conengmo/fix-geojson-identifier
Fix geojson identifier
2 parents 5d14833 + 54ebd6c commit 61bf0e9

File tree

2 files changed

+114
-10
lines changed

2 files changed

+114
-10
lines changed

folium/features.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -521,17 +521,31 @@ def _validate_function(self, func, name):
521521
.format(name))
522522

523523
def find_identifier(self):
524-
"""Find a unique identifier for each feature, create it if needed."""
525-
features = self.data['features']
526-
n = len(features)
527-
feature = features[0]
528-
if 'id' in feature and len(set(feat['id'] for feat in features)) == n:
524+
"""Find a unique identifier for each feature, create it if needed.
525+
526+
According to the GeoJSON specs a feature:
527+
- MAY have an 'id' field with a string or numerical value.
528+
- MUST have a 'properties' field. The content can be any json object
529+
or even null.
530+
531+
"""
532+
feats = self.data['features']
533+
# Each feature has an 'id' field with a unique value.
534+
unique_ids = set(feat.get('id', None) for feat in feats)
535+
if None not in unique_ids and len(unique_ids) == len(feats):
529536
return 'feature.id'
530-
for key in feature.get('properties', []):
531-
if len(set(feat['properties'][key] for feat in features)) == n:
532-
return 'feature.properties.{}'.format(key)
537+
# Each feature has a unique string or int property.
538+
if all(isinstance(feat.get('properties', None), dict) for feat in feats):
539+
for key in feats[0]['properties']:
540+
unique_values = set(
541+
feat['properties'].get(key, None) for feat in feats
542+
if isinstance(feat['properties'].get(key, None), (str, int))
543+
)
544+
if len(unique_values) == len(feats):
545+
return 'feature.properties.{}'.format(key)
546+
# We add an 'id' field with a unique value to the data.
533547
if self.embed:
534-
for i, feature in enumerate(self.data['features']):
548+
for i, feature in enumerate(feats):
535549
feature['id'] = str(i)
536550
return 'feature.id'
537551
raise ValueError(

tests/test_features.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from branca.element import Element
1313

1414
import folium
15-
from folium import Map, Popup
15+
from folium import Map, Popup, GeoJson
1616

1717
import pytest
1818

@@ -213,3 +213,93 @@ def test_geojson_tooltip():
213213
warnings.simplefilter('always')
214214
m._repr_html_()
215215
assert issubclass(w[-1].category, UserWarning), 'GeoJsonTooltip GeometryCollection test failed.'
216+
217+
218+
def test_geojson_find_identifier():
219+
220+
def _create(*properties):
221+
return {"type": "FeatureCollection", "features": [
222+
{"type": "Feature", "properties": item}
223+
for item in properties]}
224+
225+
def _assert_id_got_added(data):
226+
_geojson = GeoJson(data)
227+
assert _geojson.find_identifier() == 'feature.id'
228+
assert _geojson.data['features'][0]['id'] == '0'
229+
230+
data_with_id = _create(None, None)
231+
data_with_id['features'][0]['id'] = 'this-is-an-id'
232+
data_with_id['features'][1]['id'] = 'this-is-another-id'
233+
geojson = GeoJson(data_with_id)
234+
assert geojson.find_identifier() == 'feature.id'
235+
assert geojson.data['features'][0]['id'] == 'this-is-an-id'
236+
237+
data_with_unique_properties = _create(
238+
{'property-key': 'some-value'},
239+
{'property-key': 'another-value'},
240+
)
241+
geojson = GeoJson(data_with_unique_properties)
242+
assert geojson.find_identifier() == 'feature.properties.property-key'
243+
244+
data_with_unique_properties = _create(
245+
{'property-key': 42},
246+
{'property-key': 43},
247+
{'property-key': 'or a string'},
248+
)
249+
geojson = GeoJson(data_with_unique_properties)
250+
assert geojson.find_identifier() == 'feature.properties.property-key'
251+
252+
# The test cases below have no id field or unique property,
253+
# so an id will be added to the data.
254+
255+
data_with_identical_ids = _create(None, None)
256+
data_with_identical_ids['features'][0]['id'] = 'identical-ids'
257+
data_with_identical_ids['features'][1]['id'] = 'identical-ids'
258+
_assert_id_got_added(data_with_identical_ids)
259+
260+
data_with_some_missing_ids = _create(None, None)
261+
data_with_some_missing_ids['features'][0]['id'] = 'this-is-an-id'
262+
# the second feature doesn't have an id
263+
_assert_id_got_added(data_with_some_missing_ids)
264+
265+
data_with_identical_properties = _create(
266+
{'property-key': 'identical-value'},
267+
{'property-key': 'identical-value'},
268+
)
269+
_assert_id_got_added(data_with_identical_properties)
270+
271+
data_bare = _create(None)
272+
_assert_id_got_added(data_bare)
273+
274+
data_empty_dict = _create({})
275+
_assert_id_got_added(data_empty_dict)
276+
277+
data_without_properties = _create(None)
278+
del data_without_properties['features'][0]['properties']
279+
_assert_id_got_added(data_without_properties)
280+
281+
data_some_without_properties = _create({'key': 'value'}, 'will be deleted')
282+
# the first feature has properties, but the second doesn't
283+
del data_some_without_properties['features'][1]['properties']
284+
_assert_id_got_added(data_some_without_properties)
285+
286+
data_with_nested_properties = _create({
287+
"summary": {"distance": 343.2},
288+
"way_points": [3, 5],
289+
})
290+
_assert_id_got_added(data_with_nested_properties)
291+
292+
data_with_incompatible_properties = _create({
293+
"summary": {"distances": [0, 6], "durations": None},
294+
"way_points": [3, 5],
295+
})
296+
_assert_id_got_added(data_with_incompatible_properties)
297+
298+
data_loose_geometry = {"type": "LineString", "coordinates": [
299+
[3.961389, 43.583333], [3.968056, 43.580833], [3.974722, 43.578333],
300+
[3.986389, 43.575278], [3.998333, 43.5725], [4.163333, 43.530556],
301+
]}
302+
geojson = GeoJson(data_loose_geometry)
303+
geojson.convert_to_feature_collection()
304+
assert geojson.find_identifier() == 'feature.id'
305+
assert geojson.data['features'][0]['id'] == '0'

0 commit comments

Comments
 (0)