-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
287 improve osm maps with tag metadata #294
Changes from 16 commits
40cd612
ddb204f
d9dcaad
bf4ac93
640e4c1
d297281
88a98f6
ccaa46f
58b157a
78dd322
342e467
0d37238
ed563fe
177401f
0754155
0533e96
06f2f70
6aed5d4
5a1ccea
7a84776
3004a81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,8 @@ | |
* Find coordinates for node or way features | ||
* Plot the coordinates of a given list of node or way IDs | ||
""" | ||
import os | ||
import warnings | ||
from pathlib import Path | ||
from typing import Union | ||
|
||
|
@@ -37,6 +39,12 @@ | |
# ---------utilities----------- | ||
|
||
|
||
class PerformanceWarning(UserWarning): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think inheriting from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree |
||
"""Operation may be slow.""" | ||
|
||
pass | ||
|
||
|
||
def _compile_tags(osmium_feature): | ||
"""Return tag name value pairs. | ||
|
||
|
@@ -541,6 +549,16 @@ def __init__( | |
_is_expected_filetype( | ||
osm_pth, "osm_pth", check_existing=True, exp_ext=".pbf" | ||
) | ||
self.large_file_thresh = 50000 # 50 KB | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very small thing that doesn't really matter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree with your point. I used name mangling and updated the docstring. |
||
# implement performance warning on large OSM files. | ||
osm_size = os.path.getsize(osm_pth) | ||
if osm_size > self.large_file_thresh: | ||
warnings.warn( | ||
f"PBF file is {osm_size} bytes. Tag operations are expensive." | ||
" Consider filtering the pbf file smaller than" | ||
f" {self.large_file_thresh} bytes", | ||
PerformanceWarning, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A very small technicality, but the error message provokes the user to reduce file size < 50000, however an error isn't raised if file size is 50000. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'll still work for sizes greater than 50 KB, it just gets slow on init. It's hoped this would nudge the user to think about the size of the files prior to using FindTags as it's the slowest of all the osm flavoured classes. |
||
tags = tag_collator() | ||
classnm = tags.__class__.__name__ | ||
if classnm != "_TagHandler": | ||
|
@@ -614,6 +632,9 @@ class FindLocations: | |
Locations of nodes. | ||
__way_node_locs : dict | ||
Locations of nodes that belong to a way. | ||
_osm_pth : Union[Path, str] | ||
Path to osm file on disk. Used for method plot_ids() when include_tags | ||
is True. | ||
|
||
""" | ||
|
||
|
@@ -630,6 +651,7 @@ def __init__( | |
self.__node_locs = locs.node_locs | ||
self.__way_node_locs = locs.way_node_locs | ||
self.found_locs = dict() | ||
self._osm_pth = osm_pth | ||
|
||
def _check_is_implemented(self, user_feature: str, param_nm: str) -> None: | ||
"""If the requested feature is not node or way, raise.""" | ||
|
@@ -678,11 +700,123 @@ def check_locs_for_ids(self, ids: list, feature_type: str) -> dict: | |
) | ||
return self.found_locs | ||
|
||
def _merge_dicts_retain_dupe_keys( | ||
self, dict1: dict, dict2: dict, prepend_pattern: str = "parent_" | ||
) -> dict: | ||
"""Squish 2 dictionaries while retaining any duplicated keys. | ||
|
||
Update dict1 with key:value pairs from dict2. If duplicated keys are | ||
found in dict2, prepend the key with prepend_pattern. | ||
|
||
Parameters | ||
---------- | ||
dict1 : dict | ||
Dictionary of (child or node) tags. | ||
dict2 : dict | ||
Dictionary of (parent) tags. | ||
prepend_pattern : str | ||
A string to prepend any duplicated keys in dict_2 with. | ||
|
||
Returns | ||
------- | ||
dict | ||
A merged dictionary, retaining key:value pairs from both. | ||
|
||
""" | ||
tags_out = {} | ||
for d in [dict1, dict2]: | ||
if not isinstance(d, dict): | ||
raise TypeError(f"Expected dict but found {type(d)}: {d}") | ||
for id_, tags in dict1.items(): # child_tags is nested | ||
# find duplicated keys and prepend parent keys | ||
if dupes := set(tags.keys()).intersection(dict2.keys()): | ||
for key in dupes: | ||
dict2[f"{prepend_pattern}{key}"] = dict2.pop(key) | ||
# merge parent and child tag collections | ||
tags_out[id_] = tags | dict2 | ||
return tags_out | ||
Comment on lines
+731
to
+741
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A very good way of doing this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's neat now, but it took a lot of wiggling to get there! |
||
|
||
def _add_tag_context_to_coord_gdf( # noqa: C901 | ||
self, ids: list, feature_type: str, tooltip_nm: str | ||
) -> gpd.GeoDataFrame: | ||
"""Add a column of tooltips to the coord_gdf attribute. | ||
|
||
Handles node and way features separately. | ||
|
||
Parameters | ||
---------- | ||
ids : list | ||
A list of IDs. | ||
feature_type : str | ||
"way" or "node". | ||
tooltip_nm : str | ||
Name of the column to use for the tooltips. | ||
|
||
Returns | ||
------- | ||
None | ||
Updates `coord_gdf` attribute. | ||
|
||
""" | ||
mapping = {} | ||
parent_tags = self.tagfinder.check_tags_for_ids(ids, feature_type) | ||
self.coord_gdf[tooltip_nm] = self.coord_gdf.index.to_list() | ||
if feature_type == "way": | ||
parent_child_mapping = self.coord_gdf.index | ||
# Now we have child IDs, we need to run them through FindTags | ||
child_tags = self.tagfinder.check_tags_for_ids( | ||
[i[-1] for i in parent_child_mapping], feature_type="node" | ||
) | ||
# add in the parent tag ID to all child tags | ||
for k, v in child_tags.items(): | ||
for t in parent_child_mapping.to_flat_index(): | ||
if k == t[-1]: | ||
v["parent_id"] = t[0] | ||
# merge the parent way metadata dictionary with the child | ||
# metadata dict | ||
all_tags = parent_child_mapping.to_series().to_dict() | ||
for k, v in parent_tags.items(): | ||
# iterate over only the children for each parent node | ||
for id_ in [i for i in parent_child_mapping if i[0] == k]: | ||
all_tags[id_] = self._merge_dicts_retain_dupe_keys( | ||
{id_[-1]: child_tags[id_[-1]]}, v | ||
) | ||
# add combined tags as custom tooltips to coord_gdf. Use map | ||
# method to avoid lexsort performance warning | ||
for _, v in all_tags.items(): | ||
for k, val in v.items(): | ||
tooltips = [ | ||
f"<b>{tag}:</b> {val_}<br>" | ||
for tag, val_ in val.items() | ||
] | ||
mapping[(val["parent_id"], k)] = "".join(tooltips) | ||
|
||
elif feature_type == "node": | ||
for k, val in self.tagfinder.found_tags.items(): | ||
tooltips = [ | ||
f"<b>{tag}:</b> {val_}<br>" for tag, val_ in val.items() | ||
] | ||
mapping[k] = "".join(tooltips) | ||
|
||
self.coord_gdf[tooltip_nm] = self.coord_gdf[tooltip_nm].map(mapping) | ||
return None | ||
|
||
def plot_ids( | ||
self, | ||
ids: list, | ||
feature_type: str, | ||
crs: Union[str, int] = "epsg:4326", | ||
include_tags: bool = False, | ||
tooltip_nm: str = "custom_tooltip", | ||
tooltip_kwds: dict = {"labels": False}, | ||
tiles: str = "CartoDB positron", | ||
style_kwds: dict = { | ||
"color": "#3f5277", | ||
"fill": True, | ||
"fillOpacity": 0.3, | ||
"fillColor": "#3f5277", | ||
"weight": 4, | ||
}, | ||
Comment on lines
+817
to
+823
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this the correct approach? it assumes that a user wants to specify all of these features. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
) -> folium.Map: | ||
"""Plot coordinates for nodes or node members of a way. | ||
|
||
|
@@ -698,6 +832,26 @@ def plot_ids( | |
Whether the type of OSM feature to plot is node or way. | ||
crs : Union[str, int], optional | ||
The projection of the spatial features, by default "epsg:4326" | ||
include_tags : bool | ||
Should tag metadata be included in the map tooltips, by default | ||
False | ||
tooltip_nm : str | ||
Name to use for tooltip column in coord_gdf attribute, by default | ||
"custom_tooltip" | ||
tooltip_kwds : dict | ||
Additional tooltip styling arguments to pass to gpd explore(), by | ||
default {"labels": False} | ||
tiles : str | ||
Basemap provider tiles to use, by default "CartoDB positron" | ||
style_kwds : dict | ||
Additional map styling arguments to pass to gpd explore(), by | ||
default { | ||
"color": "#3f5277", | ||
"fill": True, | ||
"fillOpacity": 0.3, | ||
"fillColor": "#3f5277", | ||
"weight": 4, | ||
} | ||
|
||
Returns | ||
------- | ||
|
@@ -719,6 +873,7 @@ def plot_ids( | |
_type_defence(ids, "ids", list) | ||
_type_defence(feature_type, "feature_type", str) | ||
_type_defence(crs, "crs", (str, int)) | ||
_type_defence(include_tags, "include_tags", bool) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also add defences for the remainder of the new params? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree. good shout. |
||
self._check_is_implemented( | ||
user_feature=feature_type, param_nm="feature_type" | ||
) | ||
|
@@ -728,4 +883,19 @@ def plot_ids( | |
feature_type=feature_type, | ||
crs=crs, | ||
) | ||
return self.coord_gdf.explore() | ||
if not include_tags: | ||
imap = self.coord_gdf.explore(tiles=tiles, style_kwds=style_kwds) | ||
else: | ||
# retrieve tags for IDs and add them to self.coord_gdf | ||
self.tagfinder = FindTags(self._osm_pth) | ||
self._add_tag_context_to_coord_gdf( | ||
ids, feature_type, tooltip_nm=tooltip_nm | ||
) | ||
imap = self.coord_gdf.explore( | ||
tooltip=tooltip_nm, | ||
tooltip_kwds=tooltip_kwds, | ||
tiles=tiles, | ||
style_kwds=style_kwds, | ||
) | ||
|
||
return imap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'computationally expensive' rather than 'expensive' here would be a better description, in my opinion, as it provides greater context to the reader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree