Skip to content

Commit 54ebd6c

Browse files
committed
Simplify feature identifier method
1 parent 7c28b51 commit 54ebd6c

File tree

4 files changed

+81
-114
lines changed

4 files changed

+81
-114
lines changed

folium/features.py

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
none_min,
2626
get_obj_in_upper_tree,
2727
parse_options,
28-
dict_get,
2928
)
3029
from folium.vector_layers import PolyLine, path_options
3130

@@ -530,21 +529,23 @@ def find_identifier(self):
530529
or even null.
531530
532531
"""
533-
features = self.data['features']
534-
n = len(features)
535-
feature = features[0]
532+
feats = self.data['features']
536533
# Each feature has an 'id' field with a unique value.
537-
if 'id' in feature and len(set(feat['id'] for feat in features)) == n:
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):
538536
return 'feature.id'
539-
# Each feature has a unique string/float/int property.
540-
# 'properties' may be missing or None:
541-
if isinstance(feature.get('properties', None), dict):
542-
field_name = self._search_properties(features, 'properties')
543-
if field_name is not None:
544-
return field_name
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)
545546
# We add an 'id' field with a unique value to the data.
546547
if self.embed:
547-
for i, feature in enumerate(features):
548+
for i, feature in enumerate(feats):
548549
feature['id'] = str(i)
549550
return 'feature.id'
550551
raise ValueError(
@@ -553,30 +554,6 @@ def find_identifier(self):
553554
'field to your geojson data or set `embed=True`. '
554555
)
555556

556-
@classmethod
557-
def _search_properties(cls, features, *keys):
558-
"""Find a property for which each feature has a unique str/num value."""
559-
value_first_feature = dict_get(features[0], *keys)
560-
# Recursively look through the dictionary to find a unique value.
561-
if isinstance(value_first_feature, dict):
562-
for key in value_first_feature:
563-
field_name = cls._search_properties(features, *[*keys, key])
564-
if field_name is not None:
565-
return field_name
566-
# Check that all features have these keys and that the values are unique.
567-
unique_values = set()
568-
for feat in features:
569-
try:
570-
value = dict_get(feat, *keys)
571-
except TypeError:
572-
# not all features have the same properties layout
573-
return None
574-
if not isinstance(value, (str, float, int)):
575-
return None
576-
unique_values.add(value)
577-
if len(unique_values) == len(features):
578-
return '.'.join(['feature', *keys])
579-
580557
def _get_self_bounds(self):
581558
"""
582559
Computes the bounds of the object itself (not including it's children)

folium/utilities.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -490,20 +490,3 @@ def parse_options(**kwargs):
490490
return {camelize(key): value
491491
for key, value in kwargs.items()
492492
if value is not None}
493-
494-
495-
def dict_get(d, *keys):
496-
"""Return the field in dictionary d under the given keys.
497-
498-
Examples
499-
--------
500-
>>> dict_get({'hi': {'there': 42}}, 'hi', 'there')
501-
42
502-
503-
"""
504-
if len(keys) == 0:
505-
return d
506-
if not isinstance(d, dict):
507-
raise TypeError('The first argument should be a dictionary, not a {}.'
508-
.format(type(d)))
509-
return dict_get(d[keys[0]], *keys[1:])

tests/test_features.py

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -215,70 +215,91 @@ def test_geojson_tooltip():
215215
assert issubclass(w[-1].category, UserWarning), 'GeoJsonTooltip GeometryCollection test failed.'
216216

217217

218-
def test_geojson_search_properties():
219-
features = [{'properties': None} for _ in range(3)]
220-
assert GeoJson._search_properties(features, 'properties') is None
221-
features = [{'properties': {'hi': 'there'}},
222-
{'properties': {'hi': 'what'}}]
223-
assert GeoJson._search_properties(features, 'properties') == 'feature.properties.hi'
224-
features = [{'properties': {'hi': {'more': 'some value'}}},
225-
{'properties': {'hi': {'more': 'another value'}}}]
226-
assert (GeoJson._search_properties(features, 'properties')
227-
== 'feature.properties.hi.more')
228-
features = [{'properties': {'hi': 'there'}},
229-
{'properties': {'hi': 'there'}}]
230-
assert GeoJson._search_properties(features, 'properties') is None
231-
features = [{'properties': {'hi': 'there'}},
232-
{'properties': {'hi': None}}]
233-
assert GeoJson._search_properties(features, 'properties') is None
234-
features = [{'properties': {'hi': 'there'}},
235-
{'properties': 42}]
236-
assert GeoJson._search_properties(features, 'properties') is None
237-
features = [{'properties': [42, 43]},
238-
{'properties': [1, 2]}]
239-
assert GeoJson._search_properties(features, 'properties') is None
240-
241-
242218
def test_geojson_find_identifier():
243219

244-
def _create(properties):
220+
def _create(*properties):
245221
return {"type": "FeatureCollection", "features": [
246-
{"type": "Feature",
247-
"properties": properties}
248-
]}
222+
{"type": "Feature", "properties": item}
223+
for item in properties]}
249224

250-
data_bare = _create(None)
251-
data_with_id = _create(None)
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)
252231
data_with_id['features'][0]['id'] = 'this-is-an-id'
253-
data_with_unique_property = _create({
254-
'property-key': 'some-value',
255-
})
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+
256286
data_with_nested_properties = _create({
257287
"summary": {"distance": 343.2},
258288
"way_points": [3, 5],
259289
})
290+
_assert_id_got_added(data_with_nested_properties)
291+
260292
data_with_incompatible_properties = _create({
261293
"summary": {"distances": [0, 6], "durations": None},
262294
"way_points": [3, 5],
263295
})
264-
265-
geojson = GeoJson(data_with_id)
266-
assert geojson.find_identifier() == 'feature.id'
267-
geojson = GeoJson(data_bare)
268-
assert geojson.find_identifier() == 'feature.id'
269-
assert geojson.data['features'][0]['id'] == '0' # the id got added
270-
geojson = GeoJson(data_with_unique_property)
271-
assert geojson.find_identifier() == 'feature.properties.property-key'
272-
geojson = GeoJson(data_with_nested_properties)
273-
assert geojson.find_identifier() == 'feature.properties.summary.distance'
274-
geojson = GeoJson(data_with_incompatible_properties)
275-
assert geojson.find_identifier() == 'feature.id'
276-
assert geojson.data['features'][0]['id'] == '0' # the id got added
296+
_assert_id_got_added(data_with_incompatible_properties)
277297

278298
data_loose_geometry = {"type": "LineString", "coordinates": [
279299
[3.961389, 43.583333], [3.968056, 43.580833], [3.974722, 43.578333],
280300
[3.986389, 43.575278], [3.998333, 43.5725], [4.163333, 43.530556],
281301
]}
282302
geojson = GeoJson(data_loose_geometry)
283303
geojson.convert_to_feature_collection()
284-
assert geojson.find_identifier() == 'feature.id' # id got added
304+
assert geojson.find_identifier() == 'feature.id'
305+
assert geojson.data['features'][0]['id'] == '0'

tests/test_utilities.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
deep_copy,
1212
get_obj_in_upper_tree,
1313
parse_options,
14-
dict_get,
1514
)
1615

1716

@@ -155,16 +154,3 @@ def test_parse_options():
155154
assert parse_options(thing=None) == {}
156155
assert parse_options(long_thing=42) == {'longThing': 42}
157156
assert parse_options(thing=42, lst=[1, 2]) == {'thing': 42, 'lst': [1, 2]}
158-
159-
160-
def test_dict_get():
161-
assert dict_get({}) == {}
162-
assert dict_get({'hi': 'there'}) == {'hi': 'there'}
163-
assert dict_get({'hi': {'there': 42}}, 'hi', 'there') == 42
164-
assert dict_get({'hi': [1, 2, 3]}, 'hi') == [1, 2, 3]
165-
with pytest.raises(TypeError):
166-
dict_get({'hi': 42}, 'hi', 'wrong-key')
167-
with pytest.raises(TypeError):
168-
dict_get(42, 'wrong-key')
169-
with pytest.raises(KeyError):
170-
dict_get({'hi': 42}, 'wrong-key')

0 commit comments

Comments
 (0)