Skip to content

Commit c88a905

Browse files
committed
Adjust center such that objects are centered (wrt. thier pixel margins) if possible. Closes #9.
1 parent 1d0a262 commit c88a905

File tree

3 files changed

+90
-10
lines changed

3 files changed

+90
-10
lines changed

staticmaps/context.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,40 @@ def determine_center_zoom(self, width: int, height: int) -> typing.Tuple[s2spher
108108
if self._center is not None:
109109
if self._zoom is not None:
110110
return self._center, self._clamp_zoom(self._zoom)
111+
b = self.object_bounds()
112+
return self._center, self._determine_zoom(width, height, b, self._center)
113+
111114
b = self.object_bounds()
112115
if b is None:
113-
return self._center, self._clamp_zoom(self._zoom)
114-
if self._zoom is not None:
115-
return b.get_center(), self._clamp_zoom(self._zoom)
116-
if self._center is not None:
117-
b = b.union(s2sphere.LatLngRect(self._center, self._center))
116+
return None, None
117+
118+
c = self._determine_center(b)
119+
z = self._zoom
120+
if z is None:
121+
z = self._determine_zoom(width, height, b, c)
122+
if z is None:
123+
return None, None
124+
return self._adjust_center(width, height, c, z), z
125+
126+
def _determine_zoom(
127+
self, width: int, height: int, b: typing.Optional[s2sphere.LatLngRect], c: s2sphere.LatLngRect
128+
) -> typing.Optional[int]:
129+
if b is None:
130+
b = s2sphere.LatLngRect(c, c)
131+
else:
132+
b = b.union(s2sphere.LatLngRect(c, c))
118133
if b.is_point():
119-
return b.get_center(), None
134+
return self._clamp_zoom(15)
135+
120136
pixel_margin = self.extra_pixel_bounds()
121-
w = (width - 2.0 * max(pixel_margin[0], pixel_margin[2])) / self._tile_provider.tile_size()
122-
h = (height - 2.0 * max(pixel_margin[1], pixel_margin[3])) / self._tile_provider.tile_size()
137+
138+
w = (width - pixel_margin[0] - pixel_margin[2]) / self._tile_provider.tile_size()
139+
h = (height - pixel_margin[1] - pixel_margin[3]) / self._tile_provider.tile_size()
140+
# margins are bigger than target image size => ignore them
141+
if w <= 0 or h <= 0:
142+
w = width / self._tile_provider.tile_size()
143+
h = height / self._tile_provider.tile_size()
144+
123145
min_y = (1.0 - math.log(math.tan(b.lat_lo().radians) + (1.0 / math.cos(b.lat_lo().radians)))) / (2 * math.pi)
124146
max_y = (1.0 - math.log(math.tan(b.lat_hi().radians) + (1.0 / math.cos(b.lat_hi().radians)))) / (2 * math.pi)
125147
dx = (b.lng_hi().degrees - b.lng_lo().degrees) / 360.0
@@ -132,8 +154,8 @@ def determine_center_zoom(self, width: int, height: int) -> typing.Tuple[s2spher
132154
for zoom in range(1, self._tile_provider.max_zoom()):
133155
tiles = 2 ** zoom
134156
if (dx * tiles > w) or (dy * tiles > h):
135-
return self._determine_center(b), zoom - 1
136-
return self._determine_center(b), self._tile_provider.max_zoom()
157+
return self._clamp_zoom(zoom - 1)
158+
return self._clamp_zoom(15)
137159

138160
@staticmethod
139161
def _determine_center(b: s2sphere.LatLngRect) -> s2sphere.LatLng:
@@ -143,6 +165,39 @@ def _determine_center(b: s2sphere.LatLngRect) -> s2sphere.LatLng:
143165
lng = b.get_center().lng().degrees
144166
return s2sphere.LatLng.from_degrees(lat, lng)
145167

168+
def _adjust_center(self, width: int, height: int, center: s2sphere.LatLng, zoom: int) -> s2sphere.LatLng:
169+
if len(self._objects) == 0:
170+
return center
171+
172+
trans = Transformer(width, height, zoom, center, self._tile_provider.tile_size())
173+
174+
min_x = None
175+
max_x = None
176+
min_y = None
177+
max_y = None
178+
for obj in self._objects:
179+
l, t, r, b = obj.pixel_rect(trans)
180+
if min_x is None:
181+
min_x = l
182+
max_x = r
183+
min_y = t
184+
max_y = b
185+
else:
186+
min_x = min(min_x, l)
187+
max_x = max(max_x, r)
188+
min_y = min(min_y, t)
189+
max_y = max(max_y, b)
190+
assert min_x is not None
191+
assert max_x is not None
192+
assert min_y is not None
193+
assert max_y is not None
194+
195+
# margins are bigger than the image => ignore
196+
if (max_x - min_x) > width or (max_y - min_y) > height:
197+
return center
198+
199+
return trans.pixel2ll((max_x + min_x) * 0.5, (max_y + min_y) * 0.5)
200+
146201
def _fetch_tile(self, z: int, x: int, y: int) -> typing.Optional[bytes]:
147202
return self._tile_downloader.get(self._tile_provider, self._cache_dir, z, x, y)
148203

staticmaps/object.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import s2sphere # type: ignore
88

99
from .renderer import Renderer
10+
from .transformer import Transformer
1011

1112

1213
PixelBoundsT = typing.Tuple[int, int, int, int]
@@ -27,3 +28,11 @@ def bounds(self) -> s2sphere.LatLngRect:
2728
@abstractmethod
2829
def render(self, renderer: Renderer) -> None:
2930
pass
31+
32+
def pixel_rect(self, trans: Transformer) -> typing.Tuple[float, float, float, float]:
33+
"""Return the pixel rect (left, top, right, bottom) of the object when using the supplied Transformer."""
34+
bounds = self.bounds()
35+
se_x, se_y = trans.ll2pixel(bounds.get_vertex(1))
36+
nw_x, nw_y = trans.ll2pixel(bounds.get_vertex(3))
37+
l, t, r, b = self.extra_pixel_bounds()
38+
return nw_x - l, nw_y - t, se_x + r, se_y + b

staticmaps/transformer.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,29 @@ def mercator(latlng: s2sphere.LatLng) -> typing.Tuple[float, float]:
7979
lng = latlng.lng().radians
8080
return lng / (2 * math.pi) + 0.5, (1 - math.log(math.tan(lat) + (1 / math.cos(lat))) / math.pi) / 2
8181

82+
@staticmethod
83+
def mercator_inv(x: float, y: float) -> s2sphere.LatLng:
84+
x = 2 * math.pi * (x - 0.5)
85+
k = math.exp(4 * math.pi * (0.5 - y))
86+
y = math.asin((k - 1) / (k + 1))
87+
return s2sphere.LatLng(y, x)
88+
8289
def ll2t(self, latlng: s2sphere.LatLng) -> typing.Tuple[float, float]:
8390
x, y = self.mercator(latlng)
8491
return self._number_of_tiles * x, self._number_of_tiles * y
8592

93+
def t2ll(self, x: float, y: float) -> s2sphere.LatLng:
94+
return self.mercator_inv(x / self._number_of_tiles, y / self._number_of_tiles)
95+
8696
def ll2pixel(self, latlng: s2sphere.LatLng) -> typing.Tuple[float, float]:
8797
x, y = self.ll2t(latlng)
8898
s = self._tile_size
8999
x = self._width / 2 + (x - self._tile_center_x) * s
90100
y = self._height / 2 + (y - self._tile_center_y) * s
91101
return x, y
102+
103+
def pixel2ll(self, x: float, y: float) -> s2sphere.LatLng:
104+
s = self._tile_size
105+
x = (x - self._width / 2) / s + self._tile_center_x
106+
y = (y - self._height / 2) / s + self._tile_center_y
107+
return self.t2ll(x, y)

0 commit comments

Comments
 (0)