8
8
import requests
9
9
import numpy as np
10
10
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
13
12
from PIL import Image
14
13
from pyproj import Transformer
15
- from pydantic import BaseModel , validator , conlist
14
+ from pygeotile .point import Point as PygeoPoint
15
+ from pydantic import BaseModel , validator
16
16
from pydantic .class_validators import root_validator
17
17
18
18
from ..geometry import Point
27
27
logging .basicConfig (level = logging .INFO )
28
28
logger = logging .getLogger (__name__ )
29
29
30
+ #TODO: need to add pyproj, pygeotile, retry to dependencies
31
+
30
32
31
33
class EPSG (Enum ):
32
34
""" Provides the EPSG for tiled image assets that are currently supported.
@@ -61,8 +63,10 @@ def validate_bounds_not_equal(cls, bounds):
61
63
first_bound = bounds [0 ]
62
64
second_bound = bounds [1 ]
63
65
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 } " )
66
70
return bounds
67
71
68
72
#bounds are assumed to be in EPSG 4326 as that is what leaflet assumes
@@ -236,7 +240,9 @@ def _fetch_image_for_bounds(self,
236
240
y_tile_end : int ,
237
241
zoom : int ,
238
242
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.
240
246
"""
241
247
tiles = {}
242
248
if multithread :
@@ -248,16 +254,25 @@ def _fetch_image_for_bounds(self,
248
254
249
255
rows = []
250
256
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 ))
256
266
#no multithreading
257
267
else :
258
268
for x in range (x_tile_start , x_tile_end + 1 ):
259
269
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 )
261
276
262
277
rows = []
263
278
for y in range (y_tile_start , y_tile_end + 1 ):
@@ -269,26 +284,18 @@ def _fetch_image_for_bounds(self,
269
284
270
285
return np .vstack (rows )
271
286
272
- @retry (delay = 1 , tries = 6 , backoff = 2 , max_delay = 16 )
287
+ @retry (delay = 1 , tries = 5 , backoff = 2 , max_delay = 8 )
273
288
def _fetch_tile (self , x : int , y : int , z : int ) -> np .ndarray :
274
289
"""
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.
277
291
"""
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 ):
287
296
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
+ )
292
299
return decoded
293
300
294
301
def _crop_to_bounds (
@@ -334,7 +341,6 @@ def validate_zoom_levels(cls, zoom_levels):
334
341
return zoom_levels
335
342
336
343
337
- #TODO: we will need to update the [data] package to also require pyproj
338
344
class EPSGTransformer (BaseModel ):
339
345
"""Transformer class between different EPSG's. Useful when wanting to project
340
346
in different formats.
@@ -370,15 +376,52 @@ def geo_and_geo(self, src_epsg: EPSG, tgt_epsg: EPSG) -> None:
370
376
f"Cannot be used for Simple transformations. Found { src_epsg } and { tgt_epsg } "
371
377
)
372
378
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 ))
374
419
375
- #TODO
376
- def geo_and_pixel (self , src_epsg , geojson ):
377
- pass
420
+ self .transform_function = transform
378
421
379
422
def __call__ (self , point : Point ):
380
423
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 )
382
425
return Point (x = res [0 ], y = res [1 ])
383
426
else :
384
427
raise Exception ("No transformation has been set." )
0 commit comments