Skip to content

Commit

Permalink
Autogenerate Cesium map config from staging config
Browse files Browse the repository at this point in the history
Relates to #4
  • Loading branch information
robyngit committed Jul 1, 2022
1 parent 78fa631 commit e38b79e
Show file tree
Hide file tree
Showing 2 changed files with 251 additions and 0 deletions.
218 changes: 218 additions & 0 deletions pdgstaging/ConfigManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import json
import os
from .Deduplicator import deduplicate_neighbors, deduplicate_by_footprint
from .TilePathManager import TilePathManager
import warnings
from coloraide import Color

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -397,6 +400,21 @@ class ConfigManager():
'deduplicate_clip_method': 'within'
}

tiling_scheme_map = {
# A tiling scheme for geometry referenced to a simple
# GeographicProjection where longitude and latitude are directly
# mapped to X and Y. This projection is commonly known as
# geographic, equirectangular, equidistant cylindrical, or plate
# carrée.
'GeographicTilingScheme': ['WorldCRS84Quad', 'WGS1984Quad'],
# A tiling scheme for geometry referenced to a
# WebMercatorProjection, EPSG:3857. This is the tiling scheme used
# by Google Maps, Microsoft Bing Maps, and most of ESRI ArcGIS
# Online.
'WebMercatorTilingScheme': [
'WebMercatorQuad', 'WorldMercatorWGS84Quad'],
}

def __init__(self, config=None):
"""
Parameters
Expand Down Expand Up @@ -611,6 +629,200 @@ def get_resampling_methods(self):
resampling_methods.append(stat['resampling_method'])
return resampling_methods

def get_metacatui_raster_configs(self, base_url=''):
"""
Return a dictionary that can be used to configure a 3d tile layer
in a MetacatUI Cesium map.
Parameters
----------
base_url : str The url to where the layers will be hosted. If not
set then, paths will be relative starting with the TMS ID.
Returns
-------
list
The metacatui configuration objects as dicts
"""

tms = self.get('tms_id')
stats = self.get_stat_names()
index_order = list(self.get('tile_path_structure'))
ext = self.get('ext_web_tiles')
tsm = self.tiling_scheme_map
tile_manager = TilePathManager(self.original_config)
max_z = self.get_max_z()

index_map = {
'style': '',
'tms': tms,
'z': '{TileMatrix}',
'x': '{TileCol}',
'y': '{TileRow}'
}

# Get the tilingScheme
scheme = 'WebMercatorTilingScheme'
scheme_match = [s for s, t in tsm.items() if tms in t]
if len(scheme_match) == 0:
warnings.warn(f'Cesium does not support the tiling scheme: {tms},'
' using a default WebMercatorTilingScheme.')
else:
scheme = scheme_match[0]

# Get the bounds:
try:
bounds = tile_manager.get_total_bounding_box('web_tiles', max_z)
except ValueError:
try:
bounds = tile_manager.get_total_bounding_box('staged')
except ValueError:
try:
bounds = tile_manager.get_total_bounding_box(
'geotiff', max_z)
except ValueError:
try:
bounds = tile_manager.get_total_bounding_box(
'3dtiles', max_z)
except ValueError:
warnings.warn(
'Tile files could not be found. The cesium '
'rectangle option will not be set, and Cesium will'
' assume that the layer covers the entire world.')
bounds = None

layer_configs = []

for stat in stats:

# make the URl
index_map['style'] = stat
path_parts = [index_map[i] for i in index_order]
path_parts[-1] += ext
url = os.path.join(base_url, *path_parts + ext)

# Get the color palette
color_palette = self.get_stat_config(stat)['palette']

# convert all to hex codes.
colors = [self.to_hex(c).to for c in color_palette]
num_cols = len(colors)
# Get min and max. As the Cesium map doesn't support a different
# palette for each z-level yet, just use the max_z palette
minv = self.get_min(self, stat=stat, z=max_z, sub_general=True)
maxv = self.get_min(self, stat=stat, z=max_z, sub_general=True)

colors = []
for i in range(num_cols):
colors.append({
'color': colors[i],
'value': minv + (maxv - minv) * (i / (num_cols - 1))
})

layer_configs.append({
'type': 'WebMapTileServiceImageryProvider',
'label': stat,
'cesiumOptions': {
'url': url,
"tilingScheme": scheme,
"rectangle": bounds,
},
'colorPalette': {
'paletteType': 'continuous',
'property': stat,
'colors': colors
}
})

return layer_configs

def get_metacatui_3dtiles_config(self, base_url='', color=None):
"""
Return a dictionary that can be used to configure a 3d tile layer
in a MetacatUI Cesium map.
Parameters
----------
base_url : str
The url to where the layers will be hosted. If not set then,
paths will be relative starting with the TMS ID.
color : str
The color to use for the 3d tiles. If not set, then the first
color in the first configured statistic will be used, or white
if no colors are configured.
Returns
-------
dict
The metacatui configuration object as a dict.
"""

min_z = self.get_min_z()
tile_manager = TilePathManager(self.original_config)
try:
top_tree_tile = tile_manager.get_filenames_from_dir(
'3dtiles', z=min_z)
if len(top_tree_tile) == 0 or len(top_tree_tile) > 1:
raise ValueError('No 3dtiles found')
except ValueError:
raise ValueError(
'The top-most json node for the Cesium 3D tileset tree'
' could noat be found. Please check that the 3dtiles'
' dir is correctly configured, and that the workflow'
'has already run.')
top_tree_tile = top_tree_tile[0]
# remove the 3dtiles base dir
top_tree_tile = top_tree_tile.replace(self.get('dir_3dtiles'), '')
# Add the hosting base url
top_tree_tile = os.path.join(base_url, top_tree_tile)

if color is None:
pals = self.get_palettes()
if(pals and len(pals) > 0):
color = pals[0][0]
else:
color = 'white'
color = self.to_hex(color)

return {
'label': '3D Tiles',
'type': 'Cesium3DTileset',
'cesiumOptions': {
'url': top_tree_tile,
},
'colorPalette': {
'paletteType': 'categorical',
'colorPalette': {'colors': [{'color': color}]}
}
}

def get_metacatui_configs(self, base_url='', tile3d_color=None):
"""
Return a dictionary that can be used to configure a layer in a
MetacatUI Cesium map.
Parameters
----------
base_url : str
The url to where the layers will be hosted. If not set then,
paths will be relative starting with the TMS ID.
3dtile_color : str
The color to use for the 3d tiles. If not set, then the first
color in the first configured statistic will be used, or white
if no colors are configured.
Returns
-------
list
A list of metacatui configuration objects as dicts, starting
with the 3D tiles layer, followed by the raster layers.
"""

tiles3d_config = self.get_metacatui_3dtiles_config(
base_url=base_url, color=tile3d_color)
raster_configs = self.get_metacatui_raster_configs(base_url=base_url)
return [tiles3d_config] + raster_configs

def get_value_range(self, stat=None, z=None, sub_general=False):
"""
Get the value range for a statistic at a particular z level. If no
Expand Down Expand Up @@ -1153,3 +1365,9 @@ def list_updates(self):
updates.append(f'{key} removed')

return updates

def to_hex(color_str):
"""
Convert a color string to a hex string
"""
return Color(color_str).convert('sRGB').to_string(hex=True)
33 changes: 33 additions & 0 deletions pdgstaging/TilePathManager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import re
import morecantile
import geopandas as gpd
from shapely.geometry import box


class TilePathManager():
Expand Down Expand Up @@ -690,3 +692,34 @@ def get_bounding_box(self, tile=None, as_dict=True):
'right': bounding_box.right}

return bounding_box

def get_total_bounding_box(self, dir, z=None):
"""
Get the total bounding box for all tiles in a directory.
Parameters
----------
dir : str
The name of the directory to get the total bounding box for.
z : int
The zoom level to get the total bounding box for. If None, the
total bounding box for all zoom levels is returned.
Returns
-------
list
The total bounding box for the tiles in the directory, in the
unit of the CRS of the TMS set on this class.
"""

tile_paths = self.get_filenames_from_dir(dir, z=z)
tiles = [self.tile_from_path(path) for path in tile_paths]
# get the total bounds for all the tiles
bounds = [self.get_bounding_box(tile) for tile in tiles]
# get the total bounds for all the tiles
polygons = [
box(b['left'], b['bottom'], b['right'], b['top']) for b in bounds]
bounds_gs = gpd.GeoSeries(polygons, crs=self.tms.crs)
total_bounds = list(bounds_gs.total_bounds)

return total_bounds

0 comments on commit e38b79e

Please sign in to comment.