88import requests
99import numpy as np
1010
11- from retry import retry #TODO not part of the package atm. need to add in?
12- import tensorflow as f
11+ from retry import retry
1312from PIL import Image
1413from pyproj import Transformer
15- from pydantic import BaseModel , validator , conlist
14+ from pygeotile .point import Point as PygeoPoint
15+ from pydantic import BaseModel , validator
1616from pydantic .class_validators import root_validator
1717
1818from ..geometry import Point
2727logging .basicConfig (level = logging .INFO )
2828logger = logging .getLogger (__name__ )
2929
30+ #TODO: need to add pyproj, pygeotile, retry to dependencies
31+
3032
3133class EPSG (Enum ):
3234 """ Provides the EPSG for tiled image assets that are currently supported.
@@ -61,8 +63,10 @@ def validate_bounds_not_equal(cls, bounds):
6163 first_bound = bounds [0 ]
6264 second_bound = bounds [1 ]
6365
64- if first_bound == second_bound :
65- raise AssertionError (f"Bounds cannot be equal, contains { bounds } " )
66+ if first_bound .x == second_bound .x or \
67+ first_bound .y == second_bound .y :
68+ raise ValueError (
69+ f"Bounds on either axes cannot be equal, currently { bounds } " )
6670 return bounds
6771
6872 #bounds are assumed to be in EPSG 4326 as that is what leaflet assumes
@@ -236,7 +240,9 @@ def _fetch_image_for_bounds(self,
236240 y_tile_end : int ,
237241 zoom : int ,
238242 multithread = True ) -> np .ndarray :
239- """Fetches the tiles and combines them into a single image
243+ """Fetches the tiles and combines them into a single image.
244+
245+ If a tile cannot be fetched, a padding of expected tile size is instead added.
240246 """
241247 tiles = {}
242248 if multithread :
@@ -248,16 +254,25 @@ def _fetch_image_for_bounds(self,
248254
249255 rows = []
250256 for y in range (y_tile_start , y_tile_end + 1 ):
251- rows .append (
252- np .hstack ([
253- tiles [(x , y )].result ()
254- for x in range (x_tile_start , x_tile_end + 1 )
255- ]))
257+ row = []
258+ for x in range (x_tile_start , x_tile_end + 1 ):
259+ try :
260+ row .append (tiles [(x , y )].result ())
261+ except :
262+ row .append (
263+ np .zeros (shape = (self .tile_size , self .tile_size , 3 ),
264+ dtype = np .uint8 ))
265+ rows .append (np .hstack (row ))
256266 #no multithreading
257267 else :
258268 for x in range (x_tile_start , x_tile_end + 1 ):
259269 for y in range (y_tile_start , y_tile_end + 1 ):
260- tiles [(x , y )] = self ._fetch_tile (x , y , zoom )
270+ try :
271+ tiles [(x , y )] = self ._fetch_tile (x , y , zoom )
272+ except :
273+ tiles [(x , y )] = np .zeros (shape = (self .tile_size ,
274+ self .tile_size , 3 ),
275+ dtype = np .uint8 )
261276
262277 rows = []
263278 for y in range (y_tile_start , y_tile_end + 1 ):
@@ -269,26 +284,18 @@ def _fetch_image_for_bounds(self,
269284
270285 return np .vstack (rows )
271286
272- @retry (delay = 1 , tries = 6 , backoff = 2 , max_delay = 16 )
287+ @retry (delay = 1 , tries = 5 , backoff = 2 , max_delay = 8 )
273288 def _fetch_tile (self , x : int , y : int , z : int ) -> np .ndarray :
274289 """
275- Fetches the image and returns an np array. If the image cannot be fetched,
276- a padding of expected tile size is instead added.
290+ Fetches the image and returns an np array.
277291 """
278- try :
279- data = requests .get (self .tile_layer .url .format (x = x , y = y , z = z ))
280- data .raise_for_status ()
281- decoded = np .array (Image .open (BytesIO (data .content )))[..., :3 ]
282- if decoded .shape [:2 ] != (self .tile_size , self .tile_size ):
283- logger .warning (
284- f"Unexpected tile size { decoded .shape } . Results aren't guarenteed to be correct."
285- )
286- except :
292+ data = requests .get (self .tile_layer .url .format (x = x , y = y , z = z ))
293+ data .raise_for_status ()
294+ decoded = np .array (Image .open (BytesIO (data .content )))[..., :3 ]
295+ if decoded .shape [:2 ] != (self .tile_size , self .tile_size ):
287296 logger .warning (
288- f"Unable to successfully find tile. for z,x,y: { z } ,{ x } ,{ y } "
289- "Padding is being added as a result." )
290- decoded = np .zeros (shape = (self .tile_size , self .tile_size , 3 ),
291- dtype = np .uint8 )
297+ f"Unexpected tile size { decoded .shape } . Results aren't guarenteed to be correct."
298+ )
292299 return decoded
293300
294301 def _crop_to_bounds (
@@ -334,7 +341,6 @@ def validate_zoom_levels(cls, zoom_levels):
334341 return zoom_levels
335342
336343
337- #TODO: we will need to update the [data] package to also require pyproj
338344class EPSGTransformer (BaseModel ):
339345 """Transformer class between different EPSG's. Useful when wanting to project
340346 in different formats.
@@ -370,15 +376,52 @@ def geo_and_geo(self, src_epsg: EPSG, tgt_epsg: EPSG) -> None:
370376 f"Cannot be used for Simple transformations. Found { src_epsg } and { tgt_epsg } "
371377 )
372378 self .transform_function = Transformer .from_crs (src_epsg .value ,
373- tgt_epsg .value )
379+ tgt_epsg .value ).transform
380+
381+ def _get_ranges (self , bounds : np .ndarray ):
382+ """helper function to get the range between bounds.
383+
384+ returns a tuple (x_range, y_range)"""
385+ x_range = np .max (bounds [:, 0 ]) - np .min (bounds [:, 0 ])
386+ y_range = np .max (bounds [:, 1 ]) - np .min (bounds [:, 1 ])
387+ return (x_range , y_range )
388+
389+ def geo_and_pixel (self ,
390+ src_epsg ,
391+ pixel_bounds : TiledBounds ,
392+ geo_bounds : TiledBounds ,
393+ zoom = 0 ):
394+ #TODO: pixel to geo
395+ if src_epsg == EPSG .SIMPLEPIXEL :
396+ pass
397+
398+ #geo to pixel - converts a point in geo coords to pixel coords
399+ else :
400+ pixel_bounds = pixel_bounds .bounds
401+ geo_bounds = geo_bounds .bounds
402+
403+ local_bounds = np .array (
404+ [(point .x , point .y ) for point in pixel_bounds ], dtype = np .int )
405+ #convert geo bounds to pixel bounds. assumes geo bounds are in wgs84/EPS4326 per leaflet
406+ global_bounds = np .array ([
407+ PygeoPoint .from_latitude_longitude (
408+ latitude = point .y , longitude = point .x ).pixels (zoom )
409+ for point in geo_bounds
410+ ])
411+
412+ #get the range of pixels for both sets of bounds to use as a multiplification factor
413+ global_x_range , global_y_range = self ._get_ranges (global_bounds )
414+ local_x_range , local_y_range = self ._get_ranges (local_bounds )
415+
416+ def transform (x : int , y : int ):
417+ return (x * (local_x_range ) / (global_x_range ),
418+ y * (local_y_range ) / (global_y_range ))
374419
375- #TODO
376- def geo_and_pixel (self , src_epsg , geojson ):
377- pass
420+ self .transform_function = transform
378421
379422 def __call__ (self , point : Point ):
380423 if self .transform_function is not None :
381- res = self .transform_function . transform (point .x , point .y )
424+ res = self .transform_function (point .x , point .y )
382425 return Point (x = res [0 ], y = res [1 ])
383426 else :
384427 raise Exception ("No transformation has been set." )
0 commit comments