Displaying Spatial Data: Tile vs File
When spatial UDFs are called (i.e. that return spatial data like a GeoDataFrame
or an array of GeoTiff
tiles), they run and return the output of the execution like any UDF. However they can be called in two ways that influence how Fused handles them: File
and Tile
.
This is an important distinction and can be changed in Workbench at the top of the UDF editor:
We'll demonstrate the differences with a simple UDF that takes a bounds
object as input and display how it behaves differently in each mode:
@fused.udf
def udf(bounds: fused.types.Bounds):
import geopandas as gpd
from shapely.geometry import box
# Returning bounds to gdf
gdf = gpd.GeoDataFrame(geometry=[box(*bounds)], crs=4326)
return gdf
File (Viewport)
Selecting File (Viewport)
mode, the UDF runs within a single call.
If you run the example UDF, make sure to select File (Viewport)
mode in the UDF editor, execute it with "Shift + Enter" and zoom out you'll see that gdf
covers the viewport you had:
We generally recommend using File (Viewport)
mode when :
- Working with smaller datasets that fit into memory
- Wanting to load data that doesn’t move each time you pan around
- Wanting 1 seamless layer without tiling artifacts
- Wanting data at a specific resolution
Tile
By contrast, Tile
mode runs your UDF multiple times over a grid of Mercator Tiles that cover the viewport. We aim to use anywhere from 2 to 15 tiles to cover the current viewport.
Looking at the same UDF and selecting Tile
mode, we get many different tiles:
You can see a few other differences:
- This UDF is called each time you move around the Map Viewport
- We need to "Freeze" the viewport for it to stop rendering. This is in contrast to
File (Viewport)
mode where the UDF is called once and panning in Map View doesn't re-run the UDF
We generally recommend using Tile
mode when :
- Loading a lot of data at once (since the UDF is called multiple times over a smaller extent each time)
- Wanting a more dynamic, responsive panning & scrolling experience
- Wanting a dynamic resolution to be calculated for your image rendering based on your current zoom level
The mode you select for a UDF is saved with the UDF. You can decide which mode you prefer people to use for your UDF by selecting the mode in the UDF editor.
The bounds
object
A UDF may use the bounds
parameter to spatially filter datasets and load into memory only the data that corresponds to the bounds
spatial bounds. This reduces latency and data transfer costs.
Cloud-optimized formats are particularly suited for these operations - they include Cloud Optimized GeoTiff, Geoparquet, and GeoArrow.
bounds
object types
The bounds
object defines the spatial bounds of the Tile, which can be represented as a geometry object or XYZ index.
fused.types.Bounds
This is a list
of 4 points representing the bounds (extent) of a geometry. The 4 points represent [xmin, ymin, xmax, ymax]
of the bounds.
@fused.udf
def udf(bounds: fused.types.Bounds=None):
print(bounds)
>>> [-1.52244399, 48.62747869, -1.50004107, 48.64359255]
fused.types.Bounds
is a list of 4 points so it might be helpful to convert it to a GeoDataFrame
when returning spatial data:
@fused.udf
def udf(bounds: fused.types.Bounds=None):
import shapely
import geopandas as gpd
box = shapely.box(*bounds)
return gpd.GeoDataFrame(geometry=[box], crs=4326)
The fused
module also comes with many handy utils functions that allow you to quickly access these common operations. For example, you can use the bounds_to_gdf
utility function to perform the same operation as above. You can also use estimate_zoom
to estimate the zoom level that matches the bounds.
@fused.udf
def udf(bounds: fused.types.Bounds=None):
utils = fused.load("https://github.com/fusedio/udfs/tree/e74035a1/public/common/").utils
bounds = utils.bounds_to_gdf(bounds)
zoom = utils.estimate_zoom(bounds)
print(zoom)
return bounds
Legacy types
These types are still currently supported in fused
though only for legacy reasons and will soon be deprecated.
[Legacy] fused.types.Tile
This is a geopandas.GeoDataFrame with x
, y
, z
, and geometry
columns.
@fused.udf
def udf(bounds: fused.types.Tile=None):
print(bounds)
>>> x y z geometry
>>> 0 327 790 11 POLYGON ((-122.0 37.0, -122.0 37.1, -122.1 37.1, -122.1 37.0, -122.0 37.0))
[Legacy] fused.types.TileGDF
This behaves the same as fused.types.Tile
.
[Legacy] fused.types.ViewportGDF
This is a geopandas.geodataframe.GeoDataFrame with a geometry
column corresponding to the Polygon geometry of the current viewport in the Map.
@fused.udf
def udf(bbox: fused.types.ViewportGDF=None):
print(bbox)
return bbox
>>> geometry
>>> POLYGON ((-122.0 37.0, -122.0 37.1, -122.1 37.1, -122.1 37.0, -122.0 37.0))
[Legacy] bbox
object
UDFs defined using the legacy keyword bbox
are automatically now mapped to bounds
. Please update your code to use bounds
directly as this alias will be removed in a future release.
Call HTTP endpoints
A UDF called via an HTTP endpoint is invoked as File
or Tile
, depending on the URL structure.
File endpoint
This endpoint structure runs a UDF as a File
. See implementation examples with Felt and Google Sheets for vector.
https://www.fused.io/server/.../run/file?dtype_out_vector=csv
In some cases, dtype_out_vector=json
may return an error. This can happen when a GeoDataFrame without a geometry
column is being return or a Pandas DataFrame. You can bypass this by using dtype_out_vector=geojson
.
Tile endpoint
This endpoint structure runs a UDF as a Tile
. The {z}/{x}/{y}
templated path parameters correspond to the Tile's XYZ index, which Tiled web map clients dynamically populate. See implementation examples for Raster Tiles with Felt and DeckGL, and for Vector Tiles with DeckGL and Mapbox.
https://www.fused.io/server/.../run/tiles/{z}/{x}/{y}?&dtype_out_vector=csv
Tile UDF behavior in fused.run()
UDFs behave different when using fused.types.Tile
than in any other case. When passing a gpd.GeoDataFrame
. shapely.Geometry
to bounds:
fused.run(tile_udf, bounds=bounds)
Or passing X Y Z
:
fused.run(tile_udf, x=1, y=2, z=3)
The tile_udf
gets tiled and run on Web mercator XYZ tiles and then combined back together to speed up processing rather than executing a single run. This is in contrast to most other UDFs (either using no bounds
input at all or using bounds: fused.types.Bounds
) which run a single run across the given input.