Skip to content

Commit e26d89d

Browse files
committed
Refactor the rendering code to allow for easier definition of custom objects.
1 parent c88a905 commit e26d89d

File tree

11 files changed

+205
-188
lines changed

11 files changed

+205
-188
lines changed

.pylintrc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ confidence=
5353
# --disable=W"
5454
disable=
5555
bad-continuation,
56+
duplicate-code,
5657
missing-docstring,
5758
no-init,
5859
no-member,
@@ -107,13 +108,13 @@ notes=FIXME,XXX,TODO
107108
min-similarity-lines=5
108109

109110
# Ignore comments when computing similarities.
110-
ignore-comments=no
111+
ignore-comments=yes
111112

112113
# Ignore docstrings when computing similarities.
113114
ignore-docstrings=no
114115

115116
# Ignore imports when computing similarities.
116-
ignore-imports=no
117+
ignore-imports=yes
117118

118119

119120
[VARIABLES]

staticmaps/area.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import s2sphere # type: ignore
77

8+
from .cairo_renderer import CairoRenderer
89
from .color import Color, RED, TRANSPARENT
910
from .line import Line
10-
from .renderer import Renderer
11+
from .svg_renderer import SvgRenderer
1112

1213

1314
class Area(Line):
@@ -23,5 +24,39 @@ def __init__(
2324
def fill_color(self) -> Color:
2425
return self._fill_color
2526

26-
def render(self, renderer: Renderer) -> None:
27-
renderer.render_area_object(self)
27+
def render_svg(self, renderer: SvgRenderer) -> None:
28+
xys = [renderer.transformer().ll2pixel(latlng) for latlng in self.interpolate()]
29+
30+
polygon = renderer.drawing().polygon(
31+
xys,
32+
fill=self.fill_color().hex_rgb(),
33+
opacity=self.fill_color().float_a(),
34+
)
35+
renderer.group().add(polygon)
36+
37+
if self.width() > 0:
38+
polyline = renderer.drawing().polyline(
39+
xys,
40+
fill="none",
41+
stroke=self.color().hex_rgb(),
42+
stroke_width=self.width(),
43+
opacity=self.color().float_a(),
44+
)
45+
renderer.group().add(polyline)
46+
47+
def render_cairo(self, renderer: CairoRenderer) -> None:
48+
xys = [renderer.transformer().ll2pixel(latlng) for latlng in self.interpolate()]
49+
50+
renderer.context().set_source_rgba(*self.fill_color().float_rgba())
51+
renderer.context().new_path()
52+
for x, y in xys:
53+
renderer.context().line_to(x, y)
54+
renderer.context().fill()
55+
56+
if self.width() > 0:
57+
renderer.context().set_source_rgba(*self.color().float_rgba())
58+
renderer.context().set_line_width(self.width())
59+
renderer.context().new_path()
60+
for x, y in xys:
61+
renderer.context().line_to(x, y)
62+
renderer.context().stroke()

staticmaps/cairo_renderer.py

Lines changed: 28 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import cairo # type: ignore
99
from PIL import Image # type: ignore
1010

11-
from .area import Area
1211
from .color import Color, BLACK, WHITE
13-
from .image_marker import ImageMarker
14-
from .line import Line
15-
from .marker import Marker
1612
from .renderer import Renderer
1713
from .transformer import Transformer
1814

15+
if typing.TYPE_CHECKING:
16+
# avoid circlic import
17+
from .object import Object # pylint: disable=cyclic-import
18+
1919

2020
class CairoRenderer(Renderer):
2121
def __init__(self, transformer: Transformer) -> None:
@@ -27,6 +27,29 @@ def __init__(self, transformer: Transformer) -> None:
2727
def image_surface(self) -> cairo.ImageSurface:
2828
return self._surface
2929

30+
def context(self) -> cairo.Context:
31+
return self._context
32+
33+
@staticmethod
34+
def create_image(image_data: bytes) -> cairo.ImageSurface:
35+
image = Image.open(io.BytesIO(image_data))
36+
if image.format == "PNG":
37+
return cairo.ImageSurface.create_from_png(io.BytesIO(image_data))
38+
png_bytes = io.BytesIO()
39+
image.save(png_bytes, format="PNG")
40+
png_bytes.flush()
41+
png_bytes.seek(0)
42+
return cairo.ImageSurface.create_from_png(png_bytes)
43+
44+
def render_objects(self, objects: typing.List["Object"]) -> None:
45+
x_count = math.ceil(self._trans.image_width() / (2 * self._trans.world_width()))
46+
for obj in objects:
47+
for p in range(-x_count, x_count + 1):
48+
self._context.save()
49+
self._context.translate(p * self._trans.world_width(), 0)
50+
obj.render_cairo(self)
51+
self._context.restore()
52+
3053
def render_background(self, color: typing.Optional[Color]) -> None:
3154
if color is None:
3255
return
@@ -56,82 +79,6 @@ def render_tiles(self, download: typing.Callable[[int, int, int], typing.Optiona
5679
except RuntimeError:
5780
pass
5881

59-
def render_marker_object(self, marker: Marker) -> None:
60-
x, y = self._trans.ll2pixel(marker.latlng())
61-
r = marker.size()
62-
dx = math.sin(math.pi / 3.0)
63-
dy = math.cos(math.pi / 3.0)
64-
x_count = math.ceil(self._trans.image_width() / (2 * self._trans.world_width()))
65-
for p in range(-x_count, x_count + 1):
66-
self._context.save()
67-
68-
self._context.translate(p * self._trans.world_width(), 0)
69-
70-
self._context.set_source_rgb(*marker.color().text_color().float_rgb())
71-
self._context.arc(x, y - 2 * r, r, 0, 2 * math.pi)
72-
self._context.fill()
73-
self._context.new_path()
74-
self._context.line_to(x, y)
75-
self._context.line_to(x - dx * r, y - 2 * r + dy * r)
76-
self._context.line_to(x + dx * r, y - 2 * r + dy * r)
77-
self._context.close_path()
78-
self._context.fill()
79-
80-
self._context.set_source_rgb(*marker.color().float_rgb())
81-
self._context.arc(x, y - 2 * r, r - 1, 0, 2 * math.pi)
82-
self._context.fill()
83-
self._context.new_path()
84-
self._context.line_to(x, y - 1)
85-
self._context.line_to(x - dx * (r - 1), y - 2 * r + dy * (r - 1))
86-
self._context.line_to(x + dx * (r - 1), y - 2 * r + dy * (r - 1))
87-
self._context.close_path()
88-
self._context.fill()
89-
90-
self._context.restore()
91-
92-
def render_image_marker_object(self, marker: ImageMarker) -> None:
93-
x, y = self._trans.ll2pixel(marker.latlng())
94-
image = cairo.ImageSurface.create_from_png(io.BytesIO(marker.image_data()))
95-
x_count = math.ceil(self._trans.image_width() / (2 * self._trans.world_width()))
96-
for p in range(-x_count, x_count + 1):
97-
self._context.save()
98-
99-
self._context.translate(p * self._trans.world_width() + x - marker.origin_x(), y - marker.origin_y())
100-
self._context.set_source_surface(image)
101-
self._context.paint()
102-
103-
self._context.restore()
104-
105-
def render_line_object(self, line: Line) -> None:
106-
if line.width() == 0:
107-
return
108-
xys = [self._trans.ll2pixel(latlng) for latlng in line.interpolate()]
109-
x_count = math.ceil(self._trans.image_width() / (2 * self._trans.world_width()))
110-
for p in range(-x_count, x_count + 1):
111-
self._context.save()
112-
self._context.translate(p * self._trans.world_width(), 0)
113-
self._context.set_source_rgba(*line.color().float_rgba())
114-
self._context.set_line_width(line.width())
115-
self._context.new_path()
116-
for x, y in xys:
117-
self._context.line_to(x, y)
118-
self._context.stroke()
119-
self._context.restore()
120-
121-
def render_area_object(self, area: Area) -> None:
122-
xys = [self._trans.ll2pixel(latlng) for latlng in area.interpolate()]
123-
x_count = math.ceil(self._trans.image_width() / (2 * self._trans.world_width()))
124-
for p in range(-x_count, x_count + 1):
125-
self._context.save()
126-
self._context.translate(p * self._trans.world_width(), 0)
127-
self._context.new_path()
128-
for x, y in xys:
129-
self._context.line_to(x, y)
130-
self._context.set_source_rgba(*area.fill_color().float_rgba())
131-
self._context.fill()
132-
self._context.restore()
133-
self.render_line_object(area)
134-
13582
def render_attribution(self, attribution: typing.Optional[str]) -> None:
13683
if (attribution is None) or (attribution == ""):
13784
return
@@ -160,11 +107,4 @@ def fetch_tile(
160107
image_data = download(self._trans.zoom(), x, y)
161108
if image_data is None:
162109
return None
163-
image = Image.open(io.BytesIO(image_data))
164-
if image.format == "PNG":
165-
return cairo.ImageSurface.create_from_png(io.BytesIO(image_data))
166-
png_bytes = io.BytesIO()
167-
image.save(png_bytes, format="PNG")
168-
png_bytes.flush()
169-
png_bytes.seek(0)
170-
return cairo.ImageSurface.create_from_png(png_bytes)
110+
return self.create_image(image_data)

staticmaps/circle.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from .area import Area
1010
from .color import Color, RED, TRANSPARENT
1111
from .coordinates import create_latlng
12-
from .renderer import Renderer
1312

1413

1514
class Circle(Area):
@@ -23,9 +22,6 @@ def __init__(
2322
) -> None:
2423
Area.__init__(self, list(Circle.compute_circle(center, radius_km)), fill_color, color, width)
2524

26-
def render(self, renderer: Renderer) -> None:
27-
renderer.render_area_object(self)
28-
2925
@staticmethod
3026
def compute_circle(center: s2sphere.LatLng, radius_km: float) -> typing.Iterator[s2sphere.LatLng]:
3127
first = None

staticmaps/context.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import math
55
import os
6+
import sys
67
import typing
78

89
import appdirs # type: ignore
@@ -54,6 +55,9 @@ def add_object(self, obj: Object) -> None:
5455
self._objects.append(obj)
5556

5657
def render_cairo(self, width: int, height: int) -> cairo.ImageSurface:
58+
if "cairo" not in sys.modules:
59+
raise RuntimeError('You need to install the "cairo" module to enable "render_cairo".')
60+
5761
center, zoom = self.determine_center_zoom(width, height)
5862
if center is None or zoom is None:
5963
raise RuntimeError("Cannot render map without center/zoom.")

staticmaps/image_marker.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from PIL import Image # type: ignore
99

1010
from .object import Object, PixelBoundsT
11-
from .renderer import Renderer
11+
from .cairo_renderer import CairoRenderer
12+
from .svg_renderer import SvgRenderer
1213

1314

1415
class ImageMarker(Object):
@@ -58,8 +59,25 @@ def extra_pixel_bounds(self) -> PixelBoundsT:
5859
max(0, self.height() - self._origin_y),
5960
)
6061

61-
def render(self, renderer: Renderer) -> None:
62-
renderer.render_image_marker_object(self)
62+
def render_svg(self, renderer: SvgRenderer) -> None:
63+
x, y = renderer.transformer().ll2pixel(self.latlng())
64+
image = renderer.create_inline_image(self.image_data())
65+
66+
renderer.group().add(
67+
renderer.drawing().image(
68+
image,
69+
insert=(x - self.origin_x(), y - self.origin_y()),
70+
size=(self.width(), self.height()),
71+
)
72+
)
73+
74+
def render_cairo(self, renderer: CairoRenderer) -> None:
75+
x, y = renderer.transformer().ll2pixel(self.latlng())
76+
image = renderer.create_image(self.image_data())
77+
78+
renderer.context().translate(x - self.origin_x(), y - self.origin_y())
79+
renderer.context().set_source_surface(image)
80+
renderer.context().paint()
6381

6482
def load_image_data(self) -> None:
6583
with open(self._png_file, "rb") as f:

staticmaps/line.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from .color import Color, RED
1111
from .coordinates import create_latlng
1212
from .object import Object, PixelBoundsT
13-
from .renderer import Renderer
13+
from .cairo_renderer import CairoRenderer
14+
from .svg_renderer import SvgRenderer
1415

1516

1617
class Line(Object):
@@ -77,5 +78,26 @@ def interpolate(self) -> typing.List[s2sphere.LatLng]:
7778
last = current
7879
return self._interpolation_cache
7980

80-
def render(self, renderer: Renderer) -> None:
81-
renderer.render_line_object(self)
81+
def render_svg(self, renderer: SvgRenderer) -> None:
82+
if self.width() == 0:
83+
return
84+
xys = [renderer.transformer().ll2pixel(latlng) for latlng in self.interpolate()]
85+
polyline = renderer.drawing().polyline(
86+
xys,
87+
fill="none",
88+
stroke=self.color().hex_rgb(),
89+
stroke_width=self.width(),
90+
opacity=self.color().float_a(),
91+
)
92+
renderer.group().add(polyline)
93+
94+
def render_cairo(self, renderer: CairoRenderer) -> None:
95+
if self.width() == 0:
96+
return
97+
xys = [renderer.transformer().ll2pixel(latlng) for latlng in self.interpolate()]
98+
renderer.context().set_source_rgba(*self.color().float_rgba())
99+
renderer.context().set_line_width(self.width())
100+
renderer.context().new_path()
101+
for x, y in xys:
102+
renderer.context().line_to(x, y)
103+
renderer.context().stroke()

staticmaps/marker.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# py-staticmaps
22
# Copyright (c) 2020 Florian Pigorsch; see /LICENSE for licensing information
33

4+
import math
5+
46
import s2sphere # type: ignore
57

68
from .color import Color, RED
79
from .object import Object, PixelBoundsT
8-
from .renderer import Renderer
10+
from .cairo_renderer import CairoRenderer
11+
from .svg_renderer import SvgRenderer
912

1013

1114
class Marker(Object):
@@ -30,5 +33,45 @@ def bounds(self) -> s2sphere.LatLngRect:
3033
def extra_pixel_bounds(self) -> PixelBoundsT:
3134
return self._size, self._size, self._size, 0
3235

33-
def render(self, renderer: Renderer) -> None:
34-
renderer.render_marker_object(self)
36+
def render_svg(self, renderer: SvgRenderer) -> None:
37+
x, y = renderer.transformer().ll2pixel(self.latlng())
38+
r = self.size()
39+
dx = math.sin(math.pi / 3.0)
40+
dy = math.cos(math.pi / 3.0)
41+
path = renderer.drawing().path(
42+
fill=self.color().hex_rgb(),
43+
stroke=self.color().text_color().hex_rgb(),
44+
stroke_width=1,
45+
opacity=self.color().float_a(),
46+
)
47+
path.push(f"M {x} {y}")
48+
path.push(f" l {- dx * r} {- 2 * r + dy * r}")
49+
path.push(f" a {r} {r} 0 1 1 {2 * r * dx} 0")
50+
path.push("Z")
51+
renderer.group().add(path)
52+
53+
def render_cairo(self, renderer: CairoRenderer) -> None:
54+
x, y = renderer.transformer().ll2pixel(self.latlng())
55+
r = self.size()
56+
dx = math.sin(math.pi / 3.0)
57+
dy = math.cos(math.pi / 3.0)
58+
59+
renderer.context().set_source_rgb(*self.color().text_color().float_rgb())
60+
renderer.context().arc(x, y - 2 * r, r, 0, 2 * math.pi)
61+
renderer.context().fill()
62+
renderer.context().new_path()
63+
renderer.context().line_to(x, y)
64+
renderer.context().line_to(x - dx * r, y - 2 * r + dy * r)
65+
renderer.context().line_to(x + dx * r, y - 2 * r + dy * r)
66+
renderer.context().close_path()
67+
renderer.context().fill()
68+
69+
renderer.context().set_source_rgb(*self.color().float_rgb())
70+
renderer.context().arc(x, y - 2 * r, r - 1, 0, 2 * math.pi)
71+
renderer.context().fill()
72+
renderer.context().new_path()
73+
renderer.context().line_to(x, y - 1)
74+
renderer.context().line_to(x - dx * (r - 1), y - 2 * r + dy * (r - 1))
75+
renderer.context().line_to(x + dx * (r - 1), y - 2 * r + dy * (r - 1))
76+
renderer.context().close_path()
77+
renderer.context().fill()

0 commit comments

Comments
 (0)