Skip to content

Commit 9cdf58e

Browse files
adarshsmclaude
andauthored
Quote GeoJson identifier properties that aren't valid JS identifiers (#2296)
When GeoJson features have no unique `id`, `find_identifier` picks a property with unique values and emits it with dot notation in the styler and highlighter `switch`. A key like "Time (UTC)" became `feature.properties.Time (UTC)`, which Javascript runs as a call and throws `ReferenceError: UTC is not defined`, so nothing in the layer was drawn (#1782). A key like "property-key" turned into a subtraction, and a key with a dot ("osm.id") made `get_feature_id` raise `KeyError` while rendering. Keys that aren't identifiers now use bracket notation with an escaped JSON string, and `get_feature_id` reads that form back. Co-authored-by: adarshsm <24850536+adarshsm@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 42a535e commit 9cdf58e

2 files changed

Lines changed: 52 additions & 6 deletions

File tree

‎folium/features.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from folium.elements import JSCSSMixin
3434
from folium.folium import Map
3535
from folium.map import Class, FeatureGroup, Icon, Layer, Marker, Popup, Tooltip
36-
from folium.template import Template
36+
from folium.template import Template, _to_escaped_json
3737
from folium.utilities import (
3838
JsCode,
3939
TypeBoundsReturn,
@@ -829,7 +829,11 @@ def find_identifier(self) -> str:
829829
if isinstance(feat["properties"].get(key, None), (str, int))
830830
}
831831
if len(unique_values) == len(feats):
832-
return f"feature.properties.{key}"
832+
if key.isidentifier():
833+
return f"feature.properties.{key}"
834+
# Keys like "Time (UTC)" or "@id" need bracket notation
835+
# to be valid Javascript.
836+
return f"feature.properties[{_to_escaped_json(key)}]"
833837
# We add an 'id' field with a unique value to the data.
834838
if self.embed:
835839
for i, feature in enumerate(feats):
@@ -910,8 +914,13 @@ def _create_mapping(self, func: Callable, switch: str) -> TypeStyleMapping:
910914

911915
def get_feature_id(self, feature: dict) -> Union[str, int]:
912916
"""Return a value identifying the feature."""
913-
fields = self.feature_identifier.split(".")[1:]
914-
value = functools.reduce(operator.getitem, fields, feature)
917+
prefix = "feature.properties["
918+
if self.feature_identifier.startswith(prefix):
919+
key = json.loads(self.feature_identifier[len(prefix) : -1])
920+
value = feature["properties"][key]
921+
else:
922+
fields = self.feature_identifier.split(".")[1:]
923+
value = functools.reduce(operator.getitem, fields, feature)
915924
assert isinstance(value, (str, int))
916925
return value
917926

‎tests/test_features.py‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,20 +262,28 @@ def _assert_id_got_added(data):
262262
assert geojson.find_identifier() == "feature.id"
263263
assert geojson.data["features"][0]["id"] == "this-is-an-id"
264264

265+
data_with_unique_properties = _create(
266+
{"property_key": "some-value"},
267+
{"property_key": "another-value"},
268+
)
269+
geojson = GeoJson(data_with_unique_properties)
270+
assert geojson.find_identifier() == "feature.properties.property_key"
271+
272+
# Keys that aren't valid Javascript identifiers need bracket notation.
265273
data_with_unique_properties = _create(
266274
{"property-key": "some-value"},
267275
{"property-key": "another-value"},
268276
)
269277
geojson = GeoJson(data_with_unique_properties)
270-
assert geojson.find_identifier() == "feature.properties.property-key"
278+
assert geojson.find_identifier() == 'feature.properties["property-key"]'
271279

272280
data_with_unique_properties = _create(
273281
{"property-key": 42},
274282
{"property-key": 43},
275283
{"property-key": "or a string"},
276284
)
277285
geojson = GeoJson(data_with_unique_properties)
278-
assert geojson.find_identifier() == "feature.properties.property-key"
286+
assert geojson.find_identifier() == 'feature.properties["property-key"]'
279287

280288
# The test cases below have no id field or unique property,
281289
# so an id will be added to the data.
@@ -344,6 +352,35 @@ def _assert_id_got_added(data):
344352
assert geojson.data["features"][0]["id"] == "0"
345353

346354

355+
@pytest.mark.parametrize("key", ["Time (UTC)", "@id", "osm.id"])
356+
def test_geojson_style_identifier_with_special_characters(key):
357+
"""Assert #1782 is fixed"""
358+
m = Map()
359+
data = {
360+
"type": "FeatureCollection",
361+
"features": [
362+
{
363+
"type": "Feature",
364+
"properties": {key: time},
365+
"geometry": {"type": "Point", "coordinates": [0, 0]},
366+
}
367+
for time in ["10:00", "11:00", "12:00"]
368+
],
369+
}
370+
geojson = GeoJson(
371+
data,
372+
style_function=lambda x: {
373+
"color": "red" if x["properties"][key] == "10:00" else "blue"
374+
},
375+
).add_to(m)
376+
rendered = m.get_root().render()
377+
assert f"switch(feature.properties[{json.dumps(key)}])" in rendered
378+
assert geojson.style_map == {
379+
'{"color": "red"}': ["10:00"],
380+
"default": '{"color": "blue"}',
381+
}
382+
383+
347384
def test_geojson_empty_features_with_styling():
348385
# test we don't fail style function validation when there are no features
349386
m = Map()

0 commit comments

Comments
 (0)