Skip to content

Commit 632f8e0

Browse files
authored
Add option to disable Mercator calculation for OpenStreetMaps (#32)
* Add disable mercator optional kwarg * Always fave filename * Add example and update README * Clean generated example plots * Bump patch version * Add image to README * Remove obsolete save dir test * Add unittest for disable mercator * Consolidate osm conftest fixtures * change order of linting in makefile test * flake8 * Change mapper type instead of checking class var * Flake8 * Remove incorrect desc from ocstring * Update stand constructor docstring * Code formatting
1 parent 25730b8 commit 632f8e0

File tree

11 files changed

+172
-193
lines changed

11 files changed

+172
-193
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ clean:
3131
rm -rf $(PROJ_BASE)/docs/_build/*
3232
rm -f $(PROJ_BASE)/coverage.xml
3333
rm -f $(PROJ_BASE)/.coverage
34+
find $(PROJ_BASE)/examples -type f ! -name '*.py' -delete
3435

3536
PHONY: sparkling
3637
sparkling: clean
@@ -39,5 +40,5 @@ sparkling: clean
3940

4041
PHONY: test
4142
test:
42-
$(VENVPYTHON) -m flake8 .
4343
$(VENVPYTHON) -m pytest
44+
$(VENVPYTHON) -m flake8 .

README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ plot.save()
5959
---
6060
## OpenStreetMaps
6161
WKTPlot now supports the ability to integrate with OpenStreetMaps. Shape coordinates will be projected to the Mercator coordinate system, which appear to distort shape proportions compared to standard geometric projection.
62+
63+
If your shape data has already been projected, you can disable the Mercator calculation by setting the `disable_mercator` parameter when creating the plot object. See [Advanced Usage](#advanced-usage) for an example.
6264
```python
6365
# Import OpenStreetMaps plotting class
6466
from wktplot.plots.osm import OpenStreetMapsPlot
@@ -76,26 +78,42 @@ plot.save()
7678
---
7779

7880
## Advanced Usage
79-
Example for plotting from shapefile. Shapefile is of California's county boundaries from [here](https://data.ca.gov/dataset/ca-geographic-boundaries).
81+
Example for plotting from shapefile. Shapefile is of California's county boundaries, download from [here](https://data.ca.gov/dataset/ca-geographic-boundaries).
8082
```python
8183
import shapefile # pyshp module
8284

83-
from random import randrange
85+
from bokeh.palettes import Magma6
86+
from pathlib import Path
87+
from random import choice
8488
from shapely.geometry import Polygon
85-
from wktplot import WKTPlot
89+
from wktplot.plots.osm import OpenStreetMapsPlot
8690

87-
def get_rand_color():
88-
return f"#{randrange(0, 0xffffff):0>6x}"
8991

90-
plot = WKTPlot(title="California Counties", save_dir="~/scratch")
91-
with shapefile.Reader("~/scratch/CA_Counties_TIGER2016.shp") as shp:
92+
COUNTIES_PATH = Path("/path/to/CA_Counties_TIGER2016.shp")
93+
94+
# Create plot and disable mercator calculation
95+
# because data has already been projected
96+
plot = OpenStreetMapsPlot(
97+
title="California Counties 2016",
98+
height=1000,
99+
width=1000,
100+
disable_mercator=True,
101+
)
102+
103+
# Read shapefile data points from file
104+
with shapefile.Reader(COUNTIES_PATH) as shp:
92105
for shape in shp.shapes():
93-
p = Polygon(shape.points)
94-
plot.add_shape(p, fill_color=get_rand_color())
106+
plot.add_shape(
107+
shape=Polygon(shape.points),
108+
fill_color=choice(Magma6),
109+
fill_alpha=0.75,
110+
)
111+
112+
# Save plot to disk [./california_counties_2016.html]
95113
plot.save()
96114
```
97115
Which will result in this output:
98-
![CaliforniaCounties](https://i.imgur.com/YPQQlml.png)
116+
![CaliforniaCounties2016](https://i.imgur.com/lxac0JL.png)
99117

100118
---
101119

examples/advanced_shapefile.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import shapefile # pyshp module
2+
3+
from bokeh.palettes import Magma6
4+
from pathlib import Path
5+
from random import choice
6+
from shapely.geometry import Polygon
7+
from wktplot.plots.osm import OpenStreetMapsPlot
8+
9+
10+
COUNTIES_PATH = Path("/path/to/CA_Counties_TIGER2016.shp")
11+
12+
# Create plot and disable mercator calculation
13+
# because data has already been projected
14+
plot = OpenStreetMapsPlot(
15+
title="California Counties 2016",
16+
height=1000,
17+
width=1000,
18+
disable_mercator=True,
19+
)
20+
21+
# Read shapefile data points from file
22+
with shapefile.Reader(COUNTIES_PATH) as shp:
23+
for shape in shp.shapes():
24+
plot.add_shape(
25+
shape=Polygon(shape.points),
26+
fill_color=choice(Magma6),
27+
fill_alpha=0.75,
28+
)
29+
30+
# Save plot to disk [./california_counties_2016.html]
31+
plot.save()

src/wktplot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .plots.standard import WKTPlot # noqa: F401
22

3-
__version__ = "2.3.1"
3+
__version__ = "2.3.2"

src/wktplot/mappers/standard.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,6 @@
99

1010
class StandardMapper(BaseMapper):
1111

12-
@classmethod
13-
def _get_point_coords(cls, shape: Point) -> Tuple[float, float]:
14-
return shape.x, shape.y
15-
16-
@classmethod
17-
def _get_line_string_coords(cls, shape: Union[LineString, LinearRing]) -> Tuple[List[float], List[float]]:
18-
x, y = map(list, shape.xy)
19-
return x, y
20-
21-
@classmethod
22-
def _get_polygon_coords(self, shape: Polygon) -> Tuple[List[List[float]], List[List[float]]]:
23-
24-
ext_x, ext_y = map(list, shape.exterior.xy)
25-
26-
# Shape coordinates start and end with the same value.
27-
# Chop off last coordinate for Bokeh.
28-
x: List[float] = [ext_x[:-1]]
29-
y: List[float] = [ext_y[:-1]]
30-
31-
for intr_shape in shape.interiors:
32-
intr_x, intr_y = map(list, intr_shape.xy)
33-
34-
x.append(intr_x[:-1])
35-
y.append(intr_y[:-1])
36-
37-
return x, y
38-
3912
@classmethod
4013
def add_shape(cls, figure: Figure, shape: Union[str, BaseGeometry], **style_kwargs: Dict[str, Any]) -> None:
4114

@@ -65,3 +38,32 @@ def add_shape(cls, figure: Figure, shape: Union[str, BaseGeometry], **style_kwar
6538
elif isinstance(shape, Polygon):
6639
x, y = cls._get_polygon_coords(shape)
6740
figure.multi_polygons([[x]], [[y]], **style_kwargs)
41+
42+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
43+
44+
@classmethod
45+
def _get_point_coords(cls, shape: Point) -> Tuple[float, float]:
46+
return shape.x, shape.y
47+
48+
@classmethod
49+
def _get_line_string_coords(cls, shape: Union[LineString, LinearRing]) -> Tuple[List[float], List[float]]:
50+
x, y = map(list, shape.xy)
51+
return x, y
52+
53+
@classmethod
54+
def _get_polygon_coords(self, shape: Polygon) -> Tuple[List[List[float]], List[List[float]]]:
55+
56+
ext_x, ext_y = map(list, shape.exterior.xy)
57+
58+
# Shape coordinates start and end with the same value.
59+
# Chop off last coordinate for Bokeh.
60+
x: List[float] = [ext_x[:-1]]
61+
y: List[float] = [ext_y[:-1]]
62+
63+
for intr_shape in shape.interiors:
64+
intr_x, intr_y = map(list, intr_shape.xy)
65+
66+
x.append(intr_x[:-1])
67+
y.append(intr_y[:-1])
68+
69+
return x, y

src/wktplot/plots/osm.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,32 @@
22
from bokeh.tile_providers import Vendors, get_provider
33
from typing import Any, Dict
44
from wktplot.mappers.osm import OpenStreetMapper
5+
from wktplot.mappers.standard import StandardMapper
56
from wktplot.plots.standard import WKTPlot
67

78

89
class OpenStreetMapsPlot(WKTPlot):
910
""" OpenStreetMaps WKTPlot Bokeh wrapper class.
1011
"""
1112

12-
mapper = OpenStreetMapper
13-
default_figure_style_kwargs: Dict[str, str] = {
14-
"x_axis_type": "mercator",
15-
"y_axis_type": "mercator",
16-
}
13+
def __init__(self, *args, disable_mercator: bool = False, **kwargs) -> None:
14+
""" Create figure with given arguments.
15+
16+
Args:
17+
title (str): Title for graph and output filename, defaults to random string if unset.
18+
save_dir (str | obj: Path | None, default = None): Optional path to save output file to.
19+
disable_mercator (bool, default=False): Disable mercator proction calculation for shape data.
20+
**figure_style_kwargs (dict[str, Any]): Dictionary of attributes to style the created figure.
21+
See this guide for available style attributes:
22+
https://docs.bokeh.org/en/2.4.3/docs/reference/plotting/figure.html
23+
24+
Raises:
25+
ValueError: If value for `title` is not a string or None.
26+
OSError: If value for `save_dir` is not a directory.
27+
"""
28+
29+
super().__init__(*args, **kwargs)
30+
self.mapper = StandardMapper if disable_mercator else OpenStreetMapper
1731

1832
@classmethod
1933
def _create_figure(cls, title: str, **style_kwargs: Dict[str, Any]) -> plt.Figure:
@@ -22,7 +36,8 @@ def _create_figure(cls, title: str, **style_kwargs: Dict[str, Any]) -> plt.Figur
2236
# - https://docs.bokeh.org/en/latest/docs/user_guide/geo.html#tile-provider-maps
2337
default_kwargs: Dict[str, Any] = {
2438
**style_kwargs,
25-
**cls.default_figure_style_kwargs,
39+
"x_axis_type": "mercator",
40+
"y_axis_type": "mercator",
2641
}
2742

2843
tile_provider = get_provider(Vendors.OSM)

src/wktplot/plots/standard.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from bokeh import plotting as plt
22
from pathlib import Path
33
from shapely.geometry.base import BaseGeometry
4-
from typing import Any, Dict, Optional, Union
4+
from typing import Any, Dict, Union
55
from wktplot.common.file_utils import get_random_string, sanitize_text
66
from wktplot.mappers.standard import StandardMapper
77
from wktplot.plots.base import BasePlot
@@ -11,23 +11,17 @@ class WKTPlot(BasePlot):
1111
""" Standard WKTPlot Bokeh wrapper class.
1212
"""
1313

14-
mapper = StandardMapper
15-
default_figure_style_kwargs: Dict[str, str] = {
16-
"x_axis_label": "Longitude",
17-
"y_axis_label": "Latitude",
18-
}
19-
2014
def __init__(
2115
self,
2216
title: str = get_random_string(),
23-
save_dir: Optional[Union[str, Path]] = None,
17+
save_dir: Union[str, Path] = Path("."),
2418
**figure_style_kwargs: Dict[str, Any],
2519
) -> None:
2620
""" Create figure with given arguments.
2721
2822
Args:
2923
title (str): Title for graph and output filename, defaults to random string if unset.
30-
save_dir (str | obj: Path | None, default = None): Optional path to save output file to.
24+
save_dir (str | obj: Path, default = "."): Optional path to save output file to.
3125
**figure_style_kwargs (dict[str, Any]): Dictionary of attributes to style the created figure.
3226
See this guide for available style attributes:
3327
https://docs.bokeh.org/en/2.4.3/docs/reference/plotting/figure.html
@@ -40,36 +34,39 @@ def __init__(
4034
if not isinstance(title, str):
4135
raise ValueError(f"Given argument `title` is not a string. [{title}]")
4236

43-
if save_dir is not None:
44-
if isinstance(save_dir, str):
45-
save_dir = Path(save_dir)
37+
if isinstance(save_dir, str):
38+
save_dir = Path(save_dir)
4639

47-
if not (isinstance(save_dir, Path) and save_dir.is_dir()):
48-
raise OSError(f"Given argument `save_dir` is not a directory. [{save_dir}]")
40+
if not (isinstance(save_dir, Path) and save_dir.is_dir()):
41+
raise OSError(f"Given argument `save_dir` is not a directory. [{save_dir}]")
4942

50-
filename: Path = save_dir / f"{sanitize_text(title)}.html"
51-
plt.output_file(filename=filename, title=title, mode="inline")
43+
filename: Path = save_dir / f"{sanitize_text(title)}.html"
44+
plt.output_file(filename=filename, title=title, mode="inline")
5245

5346
self.figure: plt.Figure = self._create_figure(title=title, **figure_style_kwargs)
47+
self.mapper = StandardMapper
48+
49+
def add_shape(self, shape: Union[str, BaseGeometry], **style_kwargs: dict) -> None:
50+
self.mapper.add_shape(self.figure, shape, **style_kwargs)
51+
52+
def save(self) -> None:
53+
plt.save(self.figure)
54+
55+
def show(self) -> None:
56+
plt.show(self.figure)
57+
58+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
5459

5560
@classmethod
5661
def _create_figure(cls, title: str, **style_kwargs: Dict[str, Any]) -> plt.Figure:
5762

5863
default_kwargs: Dict[str, Any] = {
59-
**cls.default_figure_style_kwargs,
64+
"x_axis_label": "Longitude",
65+
"y_axis_label": "Latitude",
6066
**style_kwargs,
6167
}
6268

6369
fig = plt.figure(title=title, **default_kwargs)
6470
fig.toolbar.autohide = True
6571

6672
return fig
67-
68-
def save(self) -> None:
69-
plt.save(self.figure)
70-
71-
def show(self) -> None:
72-
plt.show(self.figure)
73-
74-
def add_shape(self, shape: Union[str, BaseGeometry], **style_kwargs: dict) -> None:
75-
self.mapper.add_shape(self.figure, shape, **style_kwargs)

0 commit comments

Comments
 (0)