Skip to content

Commit 6fcaa19

Browse files
committed
Add COG and STAC support
1 parent ffca2ca commit 6fcaa19

File tree

2 files changed

+344
-0
lines changed

2 files changed

+344
-0
lines changed

eefolium/common.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2332,6 +2332,299 @@ def load_GeoTIFFs(URLs):
23322332
return ee.ImageCollection(collection)
23332333

23342334

2335+
def get_COG_tile(url, titiler_endpoint="https://api.cogeo.xyz/", **kwargs):
2336+
"""Get a tile layer from a Cloud Optimized GeoTIFF (COG).
2337+
Source code adapted from https://developmentseed.org/titiler/examples/Working_with_CloudOptimizedGeoTIFF_simple/
2338+
2339+
Args:
2340+
url (str): HTTP URL to a COG, e.g., https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif
2341+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2342+
2343+
Returns:
2344+
tuple: Returns the COG Tile layer URL and bounds.
2345+
"""
2346+
import requests
2347+
2348+
params = {"url": url}
2349+
2350+
TileMatrixSetId='WebMercatorQuad'
2351+
if "TileMatrixSetId" in kwargs.keys():
2352+
TileMatrixSetId = kwargs["TileMatrixSetId"]
2353+
if "tile_format" in kwargs.keys():
2354+
params["tile_format"] = kwargs["tile_format"]
2355+
if "tile_scale" in kwargs.keys():
2356+
params["tile_scale"] = kwargs["tile_scale"]
2357+
if "minzoom" in kwargs.keys():
2358+
params["minzoom"] = kwargs["minzoom"]
2359+
if "maxzoom" in kwargs.keys():
2360+
params["maxzoom"] = kwargs["maxzoom"]
2361+
2362+
r = requests.get(
2363+
f"{titiler_endpoint}/cog/{TileMatrixSetId}/tilejson.json",
2364+
params = params
2365+
).json()
2366+
2367+
return r["tiles"][0]
2368+
2369+
2370+
def get_COG_bounds(url, titiler_endpoint="https://api.cogeo.xyz/"):
2371+
"""Get the bounding box of a Cloud Optimized GeoTIFF (COG).
2372+
2373+
Args:
2374+
url (str): HTTP URL to a COG, e.g., https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif
2375+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2376+
2377+
Returns:
2378+
list: A list of values representing [left, bottom, right, top]
2379+
"""
2380+
import requests
2381+
2382+
r = requests.get(
2383+
f"{titiler_endpoint}/cog/bounds",
2384+
params = {
2385+
"url": url
2386+
}
2387+
).json()
2388+
2389+
bounds = r["bounds"]
2390+
return bounds
2391+
2392+
2393+
def get_COG_center(url, titiler_endpoint="https://api.cogeo.xyz/"):
2394+
"""Get the centroid of a Cloud Optimized GeoTIFF (COG).
2395+
2396+
Args:
2397+
url (str): HTTP URL to a COG, e.g., https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif
2398+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2399+
2400+
Returns:
2401+
tuple: A tuple representing (longitude, latitude)
2402+
"""
2403+
bounds = get_COG_bounds(url, titiler_endpoint)
2404+
center=((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2) # (lat, lon)
2405+
return center
2406+
2407+
2408+
def get_COG_bands(url, titiler_endpoint="https://api.cogeo.xyz/"):
2409+
"""Get band names of a Cloud Optimized GeoTIFF (COG).
2410+
2411+
Args:
2412+
url (str): HTTP URL to a COG, e.g., https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif
2413+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2414+
2415+
Returns:
2416+
list: A list of band names
2417+
"""
2418+
import requests
2419+
2420+
r = requests.get(
2421+
f"{titiler_endpoint}/cog/info",
2422+
params = {
2423+
"url": url,
2424+
}
2425+
).json()
2426+
2427+
bands = [b[1] for b in r['band_descriptions']]
2428+
return bands
2429+
2430+
2431+
def get_STAC_tile(url, bands=None, titiler_endpoint="https://api.cogeo.xyz/", **kwargs):
2432+
"""Get a tile layer from a single SpatialTemporal Asset Catalog (STAC) item.
2433+
2434+
Args:
2435+
url (str): HTTP URL to a STAC item, e.g., https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/S5_11055_6057_20070622.json
2436+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2437+
2438+
Returns:
2439+
tuple: Returns the COG Tile layer URL and bounds.
2440+
"""
2441+
import requests
2442+
2443+
params = {"url": url}
2444+
2445+
TileMatrixSetId='WebMercatorQuad'
2446+
if "TileMatrixSetId" in kwargs.keys():
2447+
TileMatrixSetId = kwargs["TileMatrixSetId"]
2448+
if "expression" in kwargs.keys():
2449+
params["expression"] = kwargs["expression"]
2450+
if "tile_format" in kwargs.keys():
2451+
params["tile_format"] = kwargs["tile_format"]
2452+
if "tile_scale" in kwargs.keys():
2453+
params["tile_scale"] = kwargs["tile_scale"]
2454+
if "minzoom" in kwargs.keys():
2455+
params["minzoom"] = kwargs["minzoom"]
2456+
if "maxzoom" in kwargs.keys():
2457+
params["maxzoom"] = kwargs["maxzoom"]
2458+
2459+
allowed_bands = get_STAC_bands(url, titiler_endpoint)
2460+
2461+
if bands is None:
2462+
bands = [allowed_bands[0]]
2463+
elif len(bands) <= 3 and all(x in allowed_bands for x in bands):
2464+
pass
2465+
else:
2466+
raise Exception('You can only select 3 bands from the following: {}'.format(
2467+
', '.join(allowed_bands)))
2468+
2469+
assets = ','.join(bands)
2470+
params["assets"] = assets
2471+
2472+
r = requests.get(
2473+
f"{titiler_endpoint}/stac/{TileMatrixSetId}/tilejson.json",
2474+
params = params
2475+
).json()
2476+
2477+
return r["tiles"][0]
2478+
2479+
2480+
def get_STAC_bounds(url, titiler_endpoint="https://api.cogeo.xyz/"):
2481+
"""Get the bounding box of a single SpatialTemporal Asset Catalog (STAC) item.
2482+
2483+
Args:
2484+
url (str): HTTP URL to a STAC item, e.g., https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/S5_11055_6057_20070622.json
2485+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2486+
2487+
Returns:
2488+
list: A list of values representing [left, bottom, right, top]
2489+
"""
2490+
import requests
2491+
2492+
r = requests.get(
2493+
f"{titiler_endpoint}/stac/bounds",
2494+
params = {
2495+
"url": url
2496+
}
2497+
).json()
2498+
2499+
bounds = r["bounds"]
2500+
return bounds
2501+
2502+
2503+
def get_STAC_center(url, titiler_endpoint="https://api.cogeo.xyz/"):
2504+
"""Get the centroid of a single SpatialTemporal Asset Catalog (STAC) item.
2505+
2506+
Args:
2507+
url (str): HTTP URL to a STAC item, e.g., https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/S5_11055_6057_20070622.json
2508+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2509+
2510+
Returns:
2511+
tuple: A tuple representing (longitude, latitude)
2512+
"""
2513+
bounds = get_STAC_bounds(url, titiler_endpoint)
2514+
center=((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2) # (lat, lon)
2515+
return center
2516+
2517+
2518+
def get_STAC_bands(url, titiler_endpoint="https://api.cogeo.xyz/"):
2519+
"""Get band names of a single SpatialTemporal Asset Catalog (STAC) item.
2520+
2521+
Args:
2522+
url (str): HTTP URL to a STAC item, e.g., https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/S5_11055_6057_20070622.json
2523+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
2524+
2525+
Returns:
2526+
list: A list of band names
2527+
"""
2528+
import requests
2529+
2530+
r = requests.get(
2531+
f"{titiler_endpoint}/stac/info",
2532+
params = {
2533+
"url": url,
2534+
}
2535+
).json()
2536+
2537+
return r
2538+
2539+
2540+
def explode(coords):
2541+
"""Explode a GeoJSON geometry's coordinates object and yield
2542+
coordinate tuples. As long as the input is conforming, the type of
2543+
the geometry doesn't matter. From Fiona 1.4.8"""
2544+
for e in coords:
2545+
if isinstance(e, (float, int)):
2546+
yield coords
2547+
break
2548+
else:
2549+
for f in explode(e):
2550+
yield f
2551+
2552+
2553+
def get_bounds(geometry, north_up=True, transform=None):
2554+
"""Bounding box of a GeoJSON geometry, GeometryCollection, or FeatureCollection.
2555+
left, bottom, right, top
2556+
*not* xmin, ymin, xmax, ymax
2557+
If not north_up, y will be switched to guarantee the above.
2558+
Source code adapted from https://github.com/mapbox/rasterio/blob/master/rasterio/features.py#L361
2559+
"""
2560+
2561+
if 'bbox' in geometry:
2562+
return tuple(geometry['bbox'])
2563+
2564+
geometry = geometry.get('geometry') or geometry
2565+
2566+
# geometry must be a geometry, GeometryCollection, or FeatureCollection
2567+
if not ('coordinates' in geometry or 'geometries' in geometry or 'features' in geometry):
2568+
raise ValueError(
2569+
"geometry must be a GeoJSON-like geometry, GeometryCollection, "
2570+
"or FeatureCollection"
2571+
)
2572+
2573+
if 'features' in geometry:
2574+
# Input is a FeatureCollection
2575+
xmins = []
2576+
ymins = []
2577+
xmaxs = []
2578+
ymaxs = []
2579+
for feature in geometry['features']:
2580+
xmin, ymin, xmax, ymax = get_bounds(feature['geometry'])
2581+
xmins.append(xmin)
2582+
ymins.append(ymin)
2583+
xmaxs.append(xmax)
2584+
ymaxs.append(ymax)
2585+
if north_up:
2586+
return min(xmins), min(ymins), max(xmaxs), max(ymaxs)
2587+
else:
2588+
return min(xmins), max(ymaxs), max(xmaxs), min(ymins)
2589+
2590+
elif 'geometries' in geometry:
2591+
# Input is a geometry collection
2592+
xmins = []
2593+
ymins = []
2594+
xmaxs = []
2595+
ymaxs = []
2596+
for geometry in geometry['geometries']:
2597+
xmin, ymin, xmax, ymax = get_bounds(geometry)
2598+
xmins.append(xmin)
2599+
ymins.append(ymin)
2600+
xmaxs.append(xmax)
2601+
ymaxs.append(ymax)
2602+
if north_up:
2603+
return min(xmins), min(ymins), max(xmaxs), max(ymaxs)
2604+
else:
2605+
return min(xmins), max(ymaxs), max(xmaxs), min(ymins)
2606+
2607+
elif 'coordinates' in geometry:
2608+
# Input is a singular geometry object
2609+
if transform is not None:
2610+
xyz = list(explode(geometry['coordinates']))
2611+
xyz_px = [transform * point for point in xyz]
2612+
xyz = tuple(zip(*xyz_px))
2613+
return min(xyz[0]), max(xyz[1]), max(xyz[0]), min(xyz[1])
2614+
else:
2615+
xyz = tuple(zip(*list(explode(geometry['coordinates']))))
2616+
if north_up:
2617+
return min(xyz[0]), min(xyz[1]), max(xyz[0]), max(xyz[1])
2618+
else:
2619+
return min(xyz[0]), max(xyz[1]), max(xyz[0]), min(xyz[1])
2620+
2621+
# all valid inputs returned above, so whatever falls through is an error
2622+
raise ValueError(
2623+
"geometry must be a GeoJSON-like geometry, GeometryCollection, "
2624+
"or FeatureCollection"
2625+
)
2626+
2627+
23352628
def image_props(img, date_format='YYYY-MM-dd'):
23362629
"""Gets image properties.
23372630

eefolium/eefolium.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,57 @@ def add_tile_layer(
457457
except:
458458
print("Failed to add the specified TileLayer.")
459459

460+
def add_COG_layer(
461+
self,
462+
url,
463+
name='Untitled',
464+
attribution=".",
465+
opacity=1.0,
466+
shown=True,
467+
titiler_endpoint = "https://api.cogeo.xyz/",
468+
**kwargs
469+
):
470+
"""Adds a COG TileLayer to the map.
471+
472+
Args:
473+
url (str): The URL of the COG tile layer.
474+
name (str, optional): The layer name to use for the layer. Defaults to 'Untitled'.
475+
attribution (str, optional): The attribution to use. Defaults to '.'.
476+
opacity (float, optional): The opacity of the layer. Defaults to 1.
477+
shown (bool, optional): A flag indicating whether the layer should be on by default. Defaults to True.
478+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
479+
"""
480+
tile_url = get_COG_tile(url, titiler_endpoint, **kwargs)
481+
center= get_COG_center(url, titiler_endpoint) # (lon, lat)
482+
self.add_tile_layer(tiles=tile_url, name=name, attribution=attribution, opacity=opacity, shown=shown)
483+
self.set_center(lon=center[0], lat=center[1], zoom=10)
484+
485+
def add_STAC_layer(
486+
self,
487+
url,
488+
bands=None,
489+
name='Untitled',
490+
attribution=".",
491+
opacity=1.0,
492+
shown=True,
493+
titiler_endpoint = "https://api.cogeo.xyz/",
494+
**kwargs
495+
):
496+
"""Adds a STAC TileLayer to the map.
497+
498+
Args:
499+
url (str): The URL of the COG tile layer.
500+
name (str, optional): The layer name to use for the layer. Defaults to 'Untitled'.
501+
attribution (str, optional): The attribution to use. Defaults to '.'.
502+
opacity (float, optional): The opacity of the layer. Defaults to 1.
503+
shown (bool, optional): A flag indicating whether the layer should be on by default. Defaults to True.
504+
titiler_endpoint (str, optional): Titiler endpoint. Defaults to "https://api.cogeo.xyz/".
505+
"""
506+
tile_url = get_STAC_tile(url, bands, titiler_endpoint, **kwargs)
507+
center= get_STAC_center(url, titiler_endpoint)
508+
self.add_tile_layer(tiles=tile_url, name=name, attribution=attribution, opacity=opacity, shown=shown)
509+
self.set_center(lon=center[0], lat=center[1], zoom=10)
510+
460511
def publish(
461512
self,
462513
name=None,

0 commit comments

Comments
 (0)