diff --git a/folium/features.py b/folium/features.py index 2303ef3a36..587f5e053b 100644 --- a/folium/features.py +++ b/folium/features.py @@ -33,7 +33,7 @@ from folium.elements import JSCSSMixin from folium.folium import Map from folium.map import Class, FeatureGroup, Icon, Layer, Marker, Popup, Tooltip -from folium.template import Template +from folium.template import Template, _to_escaped_json from folium.utilities import ( JsCode, TypeBoundsReturn, @@ -829,7 +829,11 @@ def find_identifier(self) -> str: if isinstance(feat["properties"].get(key, None), (str, int)) } if len(unique_values) == len(feats): - return f"feature.properties.{key}" + if key.isidentifier(): + return f"feature.properties.{key}" + # Keys like "Time (UTC)" or "@id" need bracket notation + # to be valid Javascript. + return f"feature.properties[{_to_escaped_json(key)}]" # We add an 'id' field with a unique value to the data. if self.embed: for i, feature in enumerate(feats): @@ -910,8 +914,13 @@ def _create_mapping(self, func: Callable, switch: str) -> TypeStyleMapping: def get_feature_id(self, feature: dict) -> Union[str, int]: """Return a value identifying the feature.""" - fields = self.feature_identifier.split(".")[1:] - value = functools.reduce(operator.getitem, fields, feature) + prefix = "feature.properties[" + if self.feature_identifier.startswith(prefix): + key = json.loads(self.feature_identifier[len(prefix) : -1]) + value = feature["properties"][key] + else: + fields = self.feature_identifier.split(".")[1:] + value = functools.reduce(operator.getitem, fields, feature) assert isinstance(value, (str, int)) return value diff --git a/tests/test_features.py b/tests/test_features.py index 41498cabe1..e41e0e8337 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -262,12 +262,20 @@ def _assert_id_got_added(data): assert geojson.find_identifier() == "feature.id" assert geojson.data["features"][0]["id"] == "this-is-an-id" + data_with_unique_properties = _create( + {"property_key": "some-value"}, + {"property_key": "another-value"}, + ) + geojson = GeoJson(data_with_unique_properties) + assert geojson.find_identifier() == "feature.properties.property_key" + + # Keys that aren't valid Javascript identifiers need bracket notation. data_with_unique_properties = _create( {"property-key": "some-value"}, {"property-key": "another-value"}, ) geojson = GeoJson(data_with_unique_properties) - assert geojson.find_identifier() == "feature.properties.property-key" + assert geojson.find_identifier() == 'feature.properties["property-key"]' data_with_unique_properties = _create( {"property-key": 42}, @@ -275,7 +283,7 @@ def _assert_id_got_added(data): {"property-key": "or a string"}, ) geojson = GeoJson(data_with_unique_properties) - assert geojson.find_identifier() == "feature.properties.property-key" + assert geojson.find_identifier() == 'feature.properties["property-key"]' # The test cases below have no id field or unique property, # so an id will be added to the data. @@ -344,6 +352,35 @@ def _assert_id_got_added(data): assert geojson.data["features"][0]["id"] == "0" +@pytest.mark.parametrize("key", ["Time (UTC)", "@id", "osm.id"]) +def test_geojson_style_identifier_with_special_characters(key): + """Assert #1782 is fixed""" + m = Map() + data = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {key: time}, + "geometry": {"type": "Point", "coordinates": [0, 0]}, + } + for time in ["10:00", "11:00", "12:00"] + ], + } + geojson = GeoJson( + data, + style_function=lambda x: { + "color": "red" if x["properties"][key] == "10:00" else "blue" + }, + ).add_to(m) + rendered = m.get_root().render() + assert f"switch(feature.properties[{json.dumps(key)}])" in rendered + assert geojson.style_map == { + '{"color": "red"}': ["10:00"], + "default": '{"color": "blue"}', + } + + def test_geojson_empty_features_with_styling(): # test we don't fail style function validation when there are no features m = Map()