Module weavingspace.tile_map
Classes for tiling maps. Tiling
and
TiledMap
are exposed in the
public API and
respectively enable creation of a tiling and plotting of the tiling as a
multivariate map.
Classes
class TiledMap (tiling: Tiling = None,
map: geopandas.geodataframe.GeoDataFrame = None,
variables: dict[str, str] = None,
colourmaps: dict[str, str | dict] = None,
legend: bool = True,
legend_zoom: float = 1.0,
legend_dx: float = 0.0,
legend_dy: float = 0.0,
use_ellipse: bool = False,
ellipse_magnification: float = 1.0,
radial_key: bool = False,
draft_mode: bool = False,
scheme: str = 'equalinterval',
k: int = 100,
figsize: tuple[float] = (20, 15),
dpi: float = 72)-
Expand source code
@dataclass class TiledMap: """Class representing a tiled map. Should not be accessed directly, but will be created by calling `Tiling.get_tiled_map()`. After creation the variables and colourmaps attributes can be set, and then `TiledMap.render()` called to make a map. Settable attributes are explained in documentation of the `TiledMap.render()` method. Examples: Recommended usage is as follows. First, make a `TiledMap` from a `Tiling` object. tm = tiling.get_tiled_map(...) Some options in the `Tiling` constructor affect the map appearance. See `Tiling` for details. Once a `TiledMap` object exists, set options on it, either when calling `TiledMap.render()` or explicitly, i.e. tm.render(opt1 = val1, opt2 = val2, ...) or tm.opt1 = val1 tm.opt2 = val2 tm.render() Option settings are persistent, i.e. unless a new `TiledMap` object is created the option settings have to be explicitly reset to default values on subsequent calls to `TiledMap.render()`. The most important options are the `variables` and `colourmaps` settings. `variables` is a dictionary mapping `weavingspace.tileable.Tileable` tile_ids (usually "a", "b", etc.) to variable names in the data. For example, tm.variables = dict(zip(["a", "b"], ["population", "income"])) `colourmaps` is a dictionary mapping dataset variable names to the matplotlib colourmap to be used for each. For example, tm.colourmaps = dict(zip(tm.variables.values(), ["Reds", "Blues"])) See [this notebook](https://github.com/DOSull/weaving-space/blob/main/weavingspace/examples/example-tiles-cairo.ipynb) for simple usage. TODO: This more complicated example shows how categorical maps can be created. """ # these will be set at instantion by Tiling.get_tiled_map() tiling:Tiling = None """the Tiling with the required tiles""" map:gpd.GeoDataFrame = None """the GeoDataFrame on which this map is based""" variables:dict[str,str] = None """lookup from tile_id to variable names""" colourmaps:dict[str,Union[str,dict]] = None """lookup from variables to matplotlib cmaps""" # the below parameters can be set either before calling self.render() # or passed in as parameters to self.render() # these are solely TiledMap.render() options legend:bool = True """whether or not to show a legend""" legend_zoom:float = 1.0 """<1 zooms out from legend to show more context""" legend_dx:float = 0. """x shift of legend relative to the map""" legend_dy:float = 0. """y shift of legend relative to the map""" use_ellipse:bool = False """if True clips legend with an ellipse""" ellipse_magnification:float = 1.0 """magnification to apply to clip ellipse""" radial_key:bool = False """if True use radial key even for ordinal/ratio data (normally these will be shown by concentric tile geometries)""" draft_mode:bool = False """if True plot the map coloured by tile_id""" # the parameters below are geopandas.plot options which we intercept to # ensure they are applied appropriately when we plot a GDF scheme:str = "equalinterval" """geopandas scheme to apply""" k:int = 100 """geopandas number of classes to apply""" figsize:tuple[float] = (20, 15) """maptlotlib figsize""" dpi:float = 72 """dpi for bitmap formats""" def render(self, **kwargs) -> Figure: """Renders the current state to a map. Note that TiledMap objects will usually be created by calling `Tiling.get_tiled_map()`. Args: variables (dict[str,str]): Mapping from tile_id values to variable names. Defaults to None. colourmaps (dict[str,Union[str,dict]]): Mapping from variable names to colour map, either a colour palette as used by geopandas/matplotlib, a fixed colour, or a dictionary mapping categorical data values to colours. Defaults to None. legend (bool): If True a legend will be drawn. Defaults to True. legend_zoom (float): Zoom factor to apply to the legend. Values <1 will show more of the tile context. Defaults to 1.0. legend_dx (float): x shift to apply to the legend position. Defaults to 0.0. legend_dy (float): x and y shift to apply to the legend position. Defaults to 0.0. use_ellipse (bool): If True applies an elliptical clip to the legend. Defaults to False. ellipse_magnification (float): Magnification to apply to ellipse clipped legend. Defaults to 1.0. radial_key (bool): If True legend key for TileUnit maps will be based on radially dissecting the tiles. Defaults to False. draft_mode (bool): If True a map of the tiled map coloured by tile_ids (and with no legend) is returned. Defaults to False. scheme (str): passed to geopandas.plot for numeric data. Defaults to "equalinterval". k (int): passed to geopandas.plot for numeric data. Defaults to 100. figsize (tuple[float,floar]): plot dimensions passed to geopandas. plot. Defaults to (20,15). dpi (float): passed to pyplot.plot. Defaults to 72. **kwargs: other settings to pass to pyplot/geopandas.plot. Returns: matplotlib.figure.Figure: figure on which map is plotted. """ pyplot.rcParams['pdf.fonttype'] = 42 pyplot.rcParams['pdf.use14corefonts'] = True matplotlib.rcParams['pdf.fonttype'] = 42 to_remove = set() # keep track of kwargs we use to setup TiledMap for k, v in kwargs.items(): if k in self.__dict__: self.__dict__[k] = v to_remove.add(k) # remove them so we don't pass them on to pyplot and get errors for k in to_remove: del kwargs[k] if self.draft_mode: fig = pyplot.figure(figsize = self.figsize) ax = fig.add_subplot(111) self.map.plot(ax = ax, column = "tile_id", cmap = "tab20", **kwargs) return fig if self.legend: # this sizing stuff is rough and ready for now, possibly forever... reg_w, reg_h, *_ = \ tiling_utils.get_width_height_left_bottom(self.map.geometry) tile_w, tile_h, *_ = \ tiling_utils.get_width_height_left_bottom( self.tiling.tile_unit._get_legend_tiles().rotate( self.tiling.rotation, origin = (0, 0))) sf_w, sf_h = reg_w / tile_w / 3, reg_h / tile_h / 3 gskw = {"height_ratios": [sf_h * tile_h, reg_h - sf_h * tile_h], "width_ratios": [reg_w, sf_w * tile_w]} fig, axes = pyplot.subplot_mosaic( [["map", "legend"], ["map", "."]], gridspec_kw = gskw, figsize = self.figsize, layout = "constrained", **kwargs) else: fig, axes = pyplot.subplots( 1, 1, figsize = self.figsize, layout = "constrained", **kwargs) if self.variables is None: # get any floating point columns available default_columns = \ self.map.select_dtypes( include = ("float64", "int64")).columns self.variables = dict(zip(self.map.tile_id.unique(), list(default_columns))) print(f"""No variables specified, picked the first {len(self.variables)} numeric ones available.""") elif isinstance(self.variables, (list, tuple)): self.variables = dict(zip( self.tiling.tile_unit.tiles.tile_id.unique(), self.variables)) print(f"""Only a list of variables specified, assigning to available tile_ids.""") if self.colourmaps is None: self.colourmaps = {} for var in self.variables.values(): if self.map[var].dtype == pd.CategoricalDtype: self.colourmaps[var] = "tab20" print(f"""For categorical data, you should specify colour mapping explicitly.""") else: self.colourmaps[var] = "Reds" self._plot_map(axes, **kwargs) return fig def _plot_map(self, axes:pyplot.Axes, **kwargs) -> None: """Plots map to the supplied axes. Args: axes (pyplot.Axes): axes on which maps will be drawn. """ bb = self.map.geometry.total_bounds if self.legend: axes["map"].set_axis_off() axes["map"].set_xlim(bb[0], bb[2]) axes["map"].set_ylim(bb[1], bb[3]) self._plot_subsetted_gdf(axes["map"], self.map, **kwargs) self.plot_legend(ax = axes["legend"], **kwargs) if (self.legend_dx != 0 or self.legend_dx != 0): box = axes["legend"].get_position() box.x0 = box.x0 + self.legend_dx box.x1 = box.x1 + self.legend_dx box.y0 = box.y0 + self.legend_dy box.y1 = box.y1 + self.legend_dy axes["legend"].set_position(box) else: axes.set_axis_off() axes.set_xlim(bb[0], bb[2]) axes.set_ylim(bb[1], bb[3]) self._plot_subsetted_gdf(axes, self.map, **kwargs) return None def _plot_subsetted_gdf(self, ax:pyplot.Axes, gdf:gpd.GeoDataFrame, **kwargs) -> None: """Plots a gpd.GeoDataFrame multiple times based on a subsetting attribute (assumed to be "tile_id"). NOTE: used to plot both the main map _and_ the legend. Args: ax (pyplot.Axes): axes to plot to. gdf (gpd.GeoDataFrame): the GeoDataFrame to plot. Raises: Exception: if self.colourmaps cannot be parsed exception is raised. """ groups = gdf.groupby("tile_id") for id, var in self.variables.items(): subset = groups.get_group(id) # Handle custom color assignments via 'cmaps' parameter. # Result is setting 'cmap' variable used in plot command afterwards. if (isinstance(self.colourmaps[var], dict)): colormap_dict = self.colourmaps[var] data_unique_sorted = sorted(subset[var].unique()) cmap = matplotlib.colors.ListedColormap( [colormap_dict[x] for x in data_unique_sorted]) subset.plot(ax = ax, column = var, cmap = cmap, **kwargs) else: if (isinstance(self.colourmaps, (str, matplotlib.colors.Colormap, matplotlib.colors.LinearSegmentedColormap, matplotlib.colors.ListedColormap))): cmap = self.colourmaps # one palette for all ids elif (len(self.colourmaps) == 0): cmap = 'Reds' # set a default... here, to Brewer's 'Reds' elif (var not in self.colourmaps): cmap = 'Reds' # no color specified in dict, use default elif (isinstance(self.colourmaps[var], (str, matplotlib.colors.Colormap, matplotlib.colors.LinearSegmentedColormap, matplotlib.colors.ListedColormap))): cmap = self.colourmaps[var] # specified colors for this var else: raise Exception(f"""Color map for '{var}' is not a known type, but is {str(type(self.colourmaps[var]))}""") subset.plot(ax = ax, column = var, cmap = cmap, scheme = self.scheme, k = self.k, **kwargs) def to_file(self, fname:str = None) -> None: """Outputs the tiled map to a layered GPKG file. Currently delegates to `weavingspace.tiling_utils.write_map_to_layers()`. Args: fname (str, optional): Filename to write. Defaults to None. """ tiling_utils.write_map_to_layers(self.map, fname) return None def plot_legend(self, ax: pyplot.Axes = None, **kwargs) -> None: """Plots a legend for this tiled map. Args: ax (pyplot.Axes, optional): axes to draw legend. Defaults to None. """ # turn off axes (which seems also to make it impossible # to set a background colour) ax.set_axis_off() legend_tiles = self.tiling.tile_unit._get_legend_tiles() # this is a bit hacky, but we will apply the rotation to text # annotation so for TileUnits which don't need it, reverse that now if isinstance(self.tiling.tile_unit, TileUnit): legend_tiles.rotation = -self.tiling.rotation legend_key = self._get_legend_key_gdf(legend_tiles) legend_tiles.geometry = legend_tiles.geometry.rotate( self.tiling.rotation, origin = (0, 0)) if self.use_ellipse: ellipse = tiling_utils.get_bounding_ellipse( legend_tiles.geometry, mag = self.ellipse_magnification) bb = ellipse.total_bounds c = ellipse.unary_union.centroid else: bb = legend_tiles.geometry.total_bounds c = legend_tiles.geometry.unary_union.centroid # apply legend zoom - NOTE that this must be applied even # if self.legend_zoom is not == 1... ax.set_xlim(c.x + (bb[0] - c.x) / self.legend_zoom, c.x + (bb[2] - c.x) / self.legend_zoom) ax.set_ylim(c.y + (bb[1] - c.y) / self.legend_zoom, c.y + (bb[3] - c.y) / self.legend_zoom) # plot the legend key tiles (which include the data) self._plot_subsetted_gdf(ax, legend_key, lw = 0, **kwargs) for id, tile, rotn in zip(self.variables.keys(), legend_tiles.geometry, legend_tiles.rotation): c = tile.centroid ax.annotate(self.variables[id], xy = (c.x, c.y), ha = "center", va = "center", rotation_mode = "anchor", # adjust rotation to favour text reading left to right rotation = (rotn + self.tiling.rotation + 90) % 180 - 90, bbox = {"lw": 0, "fc": "#ffffff40"}) # now plot background; we include the central tiles, since in # the weave case these may not match the legend tiles context_tiles = self.tiling.tile_unit.get_local_patch(r = 2, include_0 = True).geometry.rotate(self.tiling.rotation, origin = (0, 0)) # for reasons escaping all reason... invalid polygons sometimes show up # here I think because of the rotation /shrug... in any case, this # sledgehammer should fix it # context_tiles = gpd.GeoSeries([g.simplify(1e-6) # for g in context_tiles.geometry], # crs = self.tiling.tile_unit.crs) if self.use_ellipse: context_tiles.clip(ellipse, keep_geom_type = False).plot( ax = ax, fc = "#9F9F9F3F", lw = 0.0) tiling_utils.get_tiling_edges(context_tiles.geometry).clip( ellipse, keep_geom_type = True).plot(ax = ax, ec = "#5F5F5F", lw = 1) else: context_tiles.plot(ax = ax, fc = "#9F9F9F3F", ec = "#5F5F5F", lw = 0.0) tiling_utils.get_tiling_edges(context_tiles.geometry).plot( ax = ax, ec = "#5F5F5F", lw = 1) def _get_legend_key_gdf(self, tiles:gpd.GeoDataFrame) -> gpd.GeoDataFrame: """Returns a GeoDataFrame of tiles dissected and with data assigned to the slice so a map of them can stand as a legend. 'Dissection' is handled differently by `WeaveUnit` and `TileUnit` objects and delegated to either `WeaveUnit._get_legend_key_shapes()` or `TileUnit._get_legend_key_shapes()`. Args: tiles (gpd.GeoDataFrame): the legend tiles. Returns: gpd.GeoDataFrame: with tile_id, variables and rotation attributes, and geometries of Tileable tiles sliced into a colour ramp or set of nested tiles. """ key_tiles = [] # set of tiles to form a colour key (e.g. a ramp) ids = [] # tile_ids applied to the keys unique_ids = [] # list of each tile_id used in order vals = [] # the data assigned to the key tiles rots = [] # rotation of each key tile subsets = self.map.groupby("tile_id") for (id, var), geom, rot in zip(self.variables.items(), tiles.geometry, tiles.rotation): subset = subsets.get_group(id) d = subset[var] radial = False # if the data are categorical then it's complicated... if d.dtype == pd.CategoricalDtype: radial = True and self.radial_key # desired order of categorical variable is the # color maps dictionary keys cmap = self.colourmaps[var] num_cats = len(cmap) val_order = dict(zip(cmap.keys(), range(num_cats))) # compile counts of each category freqs = [0] * num_cats for v in list(d): freqs[val_order[v]] += 1 # make list of the categories containing appropriate # counts of each in the order needed using a reverse lookup data_vals = list(val_order.keys()) data_vals = [data_vals[i] for i, f in enumerate(freqs) if f > 0] else: # any other data is easy! data_vals = sorted(d) freqs = [1] * len(data_vals) key = self.tiling.tile_unit._get_legend_key_shapes( geom, freqs, rot, radial) key_tiles.extend(key) vals.extend(data_vals) n = len(data_vals) ids.extend([id] * n) unique_ids.append(id) rots.extend([rot] * n) # finally make up a data table with all the data in all the # columns (each set of data only gets used in the subset it # applies to). This allows us to reuse the tiling_utils. # plot_subsetted_gdf() function key_data = {} for id in unique_ids: key_data[self.variables[id]] = vals key_gdf = gpd.GeoDataFrame( data = key_data | {"tile_id": ids, "rotation": rots}, crs = self.map.crs, geometry = gpd.GeoSeries(key_tiles)) key_gdf.geometry = key_gdf.rotate(self.tiling.rotation, origin = (0, 0)) return key_gdf def explore(self) -> None: """TODO: add wrapper to make tiled web map via geopandas.explore. """ return None
Class representing a tiled map. Should not be accessed directly, but will be created by calling
Tiling.get_tiled_map()
. After creation the variables and colourmaps attributes can be set, and thenTiledMap.render()
called to make a map. Settable attributes are explained in documentation of theTiledMap.render()
method.Examples
Recommended usage is as follows. First, make a
TiledMap
from aTiling
object.tm = tiling.get_tiled_map(…)
Some options in the
Tiling
constructor affect the map appearance. SeeTiling
for details.Once a
TiledMap
object exists, set options on it, either when callingTiledMap.render()
or explicitly, i.e.tm.render(opt1 = val1, opt2 = val2, …)
or
tm.opt1 = val1 tm.opt2 = val2 tm.render()
Option settings are persistent, i.e. unless a new
TiledMap
object is created the option settings have to be explicitly reset to default values on subsequent calls toTiledMap.render()
.The most important options are the
variables
andcolourmaps
settings.variables
is a dictionary mappingTileable
tile_ids (usually "a", "b", etc.) to variable names in the data. For example,tm.variables = dict(zip(["a", "b"], ["population", "income"]))
colourmaps
is a dictionary mapping dataset variable names to the matplotlib colourmap to be used for each. For example,tm.colourmaps = dict(zip(tm.variables.values(), ["Reds", "Blues"]))
See this notebook for simple usage.
TODO: This more complicated example shows how categorical maps can be created.
Class variables
var colourmaps : dict[str, str | dict]
-
lookup from variables to matplotlib cmaps
var dpi : float
-
dpi for bitmap formats
var draft_mode : bool
-
if True plot the map coloured by tile_id
var ellipse_magnification : float
-
magnification to apply to clip ellipse
var figsize : tuple[float]
-
maptlotlib figsize
var k : int
-
geopandas number of classes to apply
var legend : bool
-
whether or not to show a legend
var legend_dx : float
-
x shift of legend relative to the map
var legend_dy : float
-
y shift of legend relative to the map
var legend_zoom : float
-
<1 zooms out from legend to show more context
var map : geopandas.geodataframe.GeoDataFrame
-
the GeoDataFrame on which this map is based
var radial_key : bool
-
if True use radial key even for ordinal/ratio data (normally these will be shown by concentric tile geometries)
var scheme : str
-
geopandas scheme to apply
var tiling : Tiling
-
the Tiling with the required tiles
var use_ellipse : bool
-
if True clips legend with an ellipse
var variables : dict[str, str]
-
lookup from tile_id to variable names
Methods
def explore(self) ‑> None
-
Expand source code
def explore(self) -> None: """TODO: add wrapper to make tiled web map via geopandas.explore. """ return None
TODO: add wrapper to make tiled web map via geopandas.explore.
def plot_legend(self, ax: matplotlib.axes._axes.Axes = None, **kwargs) ‑> None
-
Expand source code
def plot_legend(self, ax: pyplot.Axes = None, **kwargs) -> None: """Plots a legend for this tiled map. Args: ax (pyplot.Axes, optional): axes to draw legend. Defaults to None. """ # turn off axes (which seems also to make it impossible # to set a background colour) ax.set_axis_off() legend_tiles = self.tiling.tile_unit._get_legend_tiles() # this is a bit hacky, but we will apply the rotation to text # annotation so for TileUnits which don't need it, reverse that now if isinstance(self.tiling.tile_unit, TileUnit): legend_tiles.rotation = -self.tiling.rotation legend_key = self._get_legend_key_gdf(legend_tiles) legend_tiles.geometry = legend_tiles.geometry.rotate( self.tiling.rotation, origin = (0, 0)) if self.use_ellipse: ellipse = tiling_utils.get_bounding_ellipse( legend_tiles.geometry, mag = self.ellipse_magnification) bb = ellipse.total_bounds c = ellipse.unary_union.centroid else: bb = legend_tiles.geometry.total_bounds c = legend_tiles.geometry.unary_union.centroid # apply legend zoom - NOTE that this must be applied even # if self.legend_zoom is not == 1... ax.set_xlim(c.x + (bb[0] - c.x) / self.legend_zoom, c.x + (bb[2] - c.x) / self.legend_zoom) ax.set_ylim(c.y + (bb[1] - c.y) / self.legend_zoom, c.y + (bb[3] - c.y) / self.legend_zoom) # plot the legend key tiles (which include the data) self._plot_subsetted_gdf(ax, legend_key, lw = 0, **kwargs) for id, tile, rotn in zip(self.variables.keys(), legend_tiles.geometry, legend_tiles.rotation): c = tile.centroid ax.annotate(self.variables[id], xy = (c.x, c.y), ha = "center", va = "center", rotation_mode = "anchor", # adjust rotation to favour text reading left to right rotation = (rotn + self.tiling.rotation + 90) % 180 - 90, bbox = {"lw": 0, "fc": "#ffffff40"}) # now plot background; we include the central tiles, since in # the weave case these may not match the legend tiles context_tiles = self.tiling.tile_unit.get_local_patch(r = 2, include_0 = True).geometry.rotate(self.tiling.rotation, origin = (0, 0)) # for reasons escaping all reason... invalid polygons sometimes show up # here I think because of the rotation /shrug... in any case, this # sledgehammer should fix it # context_tiles = gpd.GeoSeries([g.simplify(1e-6) # for g in context_tiles.geometry], # crs = self.tiling.tile_unit.crs) if self.use_ellipse: context_tiles.clip(ellipse, keep_geom_type = False).plot( ax = ax, fc = "#9F9F9F3F", lw = 0.0) tiling_utils.get_tiling_edges(context_tiles.geometry).clip( ellipse, keep_geom_type = True).plot(ax = ax, ec = "#5F5F5F", lw = 1) else: context_tiles.plot(ax = ax, fc = "#9F9F9F3F", ec = "#5F5F5F", lw = 0.0) tiling_utils.get_tiling_edges(context_tiles.geometry).plot( ax = ax, ec = "#5F5F5F", lw = 1)
Plots a legend for this tiled map.
Args
ax
:pyplot.Axes
, optional- axes to draw legend. Defaults to None.
def render(self, **kwargs) ‑> matplotlib.figure.Figure
-
Expand source code
def render(self, **kwargs) -> Figure: """Renders the current state to a map. Note that TiledMap objects will usually be created by calling `Tiling.get_tiled_map()`. Args: variables (dict[str,str]): Mapping from tile_id values to variable names. Defaults to None. colourmaps (dict[str,Union[str,dict]]): Mapping from variable names to colour map, either a colour palette as used by geopandas/matplotlib, a fixed colour, or a dictionary mapping categorical data values to colours. Defaults to None. legend (bool): If True a legend will be drawn. Defaults to True. legend_zoom (float): Zoom factor to apply to the legend. Values <1 will show more of the tile context. Defaults to 1.0. legend_dx (float): x shift to apply to the legend position. Defaults to 0.0. legend_dy (float): x and y shift to apply to the legend position. Defaults to 0.0. use_ellipse (bool): If True applies an elliptical clip to the legend. Defaults to False. ellipse_magnification (float): Magnification to apply to ellipse clipped legend. Defaults to 1.0. radial_key (bool): If True legend key for TileUnit maps will be based on radially dissecting the tiles. Defaults to False. draft_mode (bool): If True a map of the tiled map coloured by tile_ids (and with no legend) is returned. Defaults to False. scheme (str): passed to geopandas.plot for numeric data. Defaults to "equalinterval". k (int): passed to geopandas.plot for numeric data. Defaults to 100. figsize (tuple[float,floar]): plot dimensions passed to geopandas. plot. Defaults to (20,15). dpi (float): passed to pyplot.plot. Defaults to 72. **kwargs: other settings to pass to pyplot/geopandas.plot. Returns: matplotlib.figure.Figure: figure on which map is plotted. """ pyplot.rcParams['pdf.fonttype'] = 42 pyplot.rcParams['pdf.use14corefonts'] = True matplotlib.rcParams['pdf.fonttype'] = 42 to_remove = set() # keep track of kwargs we use to setup TiledMap for k, v in kwargs.items(): if k in self.__dict__: self.__dict__[k] = v to_remove.add(k) # remove them so we don't pass them on to pyplot and get errors for k in to_remove: del kwargs[k] if self.draft_mode: fig = pyplot.figure(figsize = self.figsize) ax = fig.add_subplot(111) self.map.plot(ax = ax, column = "tile_id", cmap = "tab20", **kwargs) return fig if self.legend: # this sizing stuff is rough and ready for now, possibly forever... reg_w, reg_h, *_ = \ tiling_utils.get_width_height_left_bottom(self.map.geometry) tile_w, tile_h, *_ = \ tiling_utils.get_width_height_left_bottom( self.tiling.tile_unit._get_legend_tiles().rotate( self.tiling.rotation, origin = (0, 0))) sf_w, sf_h = reg_w / tile_w / 3, reg_h / tile_h / 3 gskw = {"height_ratios": [sf_h * tile_h, reg_h - sf_h * tile_h], "width_ratios": [reg_w, sf_w * tile_w]} fig, axes = pyplot.subplot_mosaic( [["map", "legend"], ["map", "."]], gridspec_kw = gskw, figsize = self.figsize, layout = "constrained", **kwargs) else: fig, axes = pyplot.subplots( 1, 1, figsize = self.figsize, layout = "constrained", **kwargs) if self.variables is None: # get any floating point columns available default_columns = \ self.map.select_dtypes( include = ("float64", "int64")).columns self.variables = dict(zip(self.map.tile_id.unique(), list(default_columns))) print(f"""No variables specified, picked the first {len(self.variables)} numeric ones available.""") elif isinstance(self.variables, (list, tuple)): self.variables = dict(zip( self.tiling.tile_unit.tiles.tile_id.unique(), self.variables)) print(f"""Only a list of variables specified, assigning to available tile_ids.""") if self.colourmaps is None: self.colourmaps = {} for var in self.variables.values(): if self.map[var].dtype == pd.CategoricalDtype: self.colourmaps[var] = "tab20" print(f"""For categorical data, you should specify colour mapping explicitly.""") else: self.colourmaps[var] = "Reds" self._plot_map(axes, **kwargs) return fig
Renders the current state to a map.
Note that TiledMap objects will usually be created by calling
Tiling.get_tiled_map()
.Args
variables
:dict[str,str]
- Mapping from tile_id values to variable names. Defaults to None.
colourmaps
:dict[str,Union[str,dict]]
- Mapping from variable names to colour map, either a colour palette as used by geopandas/matplotlib, a fixed colour, or a dictionary mapping categorical data values to colours. Defaults to None.
legend
:bool
- If True a legend will be drawn. Defaults to True.
legend_zoom
:float
- Zoom factor to apply to the legend. Values <1 will show more of the tile context. Defaults to 1.0.
legend_dx
:float
- x shift to apply to the legend position. Defaults to 0.0.
legend_dy
:float
- x and y shift to apply to the legend position. Defaults to 0.0.
use_ellipse
:bool
- If True applies an elliptical clip to the legend. Defaults to False.
ellipse_magnification
:float
- Magnification to apply to ellipse clipped legend. Defaults to 1.0.
radial_key
:bool
- If True legend key for TileUnit maps will be based on radially dissecting the tiles. Defaults to False.
draft_mode
:bool
- If True a map of the tiled map coloured by tile_ids (and with no legend) is returned. Defaults to False.
scheme
:str
- passed to geopandas.plot for numeric data. Defaults to "equalinterval".
k
:int
- passed to geopandas.plot for numeric data. Defaults to 100.
figsize
:tuple[float,floar]
- plot dimensions passed to geopandas. plot. Defaults to (20,15).
dpi
:float
- passed to pyplot.plot. Defaults to 72.
**kwargs
- other settings to pass to pyplot/geopandas.plot.
Returns
matplotlib.figure.Figure
- figure on which map is plotted.
def to_file(self, fname: str = None) ‑> None
-
Expand source code
def to_file(self, fname:str = None) -> None: """Outputs the tiled map to a layered GPKG file. Currently delegates to `weavingspace.tiling_utils.write_map_to_layers()`. Args: fname (str, optional): Filename to write. Defaults to None. """ tiling_utils.write_map_to_layers(self.map, fname) return None
Outputs the tiled map to a layered GPKG file.
Currently delegates to
write_map_to_layers()
.Args
fname
:str
, optional- Filename to write. Defaults to None.
class Tiling (unit: Tileable,
region: geopandas.geodataframe.GeoDataFrame,
id_var=None,
prototile_margin: float = 0,
tiles_sf: float = 1,
tiles_margin: float = 0,
as_icons: bool = False)-
Expand source code
@dataclass class Tiling: """Class that applies a `Tileable` object to a region to be mapped. The result of the tiling procedure is stored in the `tiles` variable and covers a region sufficient that the tiling can be rotated to any desired angle. """ tile_unit:Tileable = None """tileable on which the tiling is based.""" tile_shape:TileShape = None """base shape of the tileable.""" region:gpd.GeoDataFrame = None """the region to be tiled.""" region_union: geom.Polygon = None grid:_TileGrid = None """the grid which will be used to apply the tiling.""" tiles:gpd.GeoDataFrame = None """the tiles after tiling has been carried out.""" prototiles:gpd.GeoDataFrame = None """the prototiles after tiling has been carried out.""" rotation:float = 0.0 """the cumulative rotation already applied to the tiling.""" def __init__(self, unit:Tileable, region:gpd.GeoDataFrame, id_var = None, prototile_margin:float = 0, tiles_sf:float = 1, tiles_margin:float = 0, as_icons:bool = False) -> None: """Class to persist a tiling by filling an area relative to a region sufficient to apply the tiling at any rotation. The Tiling constructor allows a number of adjustments to the supplied `weavingspace.tileable.Tileable` object: + `prototile_margin` values greater than 0 will introduce spacing of the specified distance between tiles on the boundary of each tile by applying the `TileUnit.inset_prototile()` method. Note that this operation does not make sense for `WeaveUnit` objects, and may not preserve the equality of tile areas. + `tiles_sf` values less than one scale down tiles by applying the `TileUnit.scale_tiles()` method. Does not make sense for `WeaveUnit` objects. + `tiles_margin` values greater than one apply a negative buffer of the specified distance to every tile in the tiling by applying the `Tileable.inset_tiles()` method. This option is applicable to both `WeaveUnit` and `TileUnit` objects. Args: unit (Tileable): the tile_unit to use. region (gpd.GeoDataFrame): the region to be tiled. prototile_margin (float, optional): values greater than 0 apply an inset margin to the tile unit. Defaults to 0. tiles_sf (float, optional): scales the tiles. Defaults to 1. tiles_margin (float, optional): applies a negative buffer to the tiles. Defaults to 0. as_icons (bool, optional): if True prototiles will only be placed at the region's zone centroids, one per zone. Defaults to False. """ self.tile_unit = unit self.rotation = self.tile_unit.rotation if tiles_margin > 0: self.tile_unit = self.tile_unit.inset_tiles(tiles_margin) if tiles_sf != 1: if isinstance(self.tile_unit, TileUnit): self.tile_unit = self.tile_unit.scale_tiles(tiles_sf) else: print(f"""Applying scaling to tiles of a WeaveUnit does not make sense. Ignoring tiles_sf setting of {tiles_sf}.""") if prototile_margin > 0: if isinstance(self.tile_unit, TileUnit): self.tile_unit = self.tile_unit.inset_prototile(prototile_margin) else: print(f"""Applying a prototile margin to a WeaveUnit does not make sense. Ignoring prototile_margin setting of {prototile_margin}.""") self.region = region self.region.sindex self.region_union = self.region.geometry.unary_union if id_var != None: print("""id_var is no longer required and will be deprecated soon. A temporary unique index attribute is added and removed when generating the tiled map.""") if as_icons: self.grid = _TileGrid(self.tile_unit, self.region.geometry, True) else: self.grid = _TileGrid(self.tile_unit, self.region.geometry) self.tiles, self.prototiles = self.make_tiling() self.tiles.sindex def get_tiled_map(self, rotation:float = 0., join_on_prototiles:bool = True, prioritise_tiles:bool = True, ragged_edges:bool = True, use_centroid_lookup_approximation = False, debug = False) -> "TiledMap": """Returns a `TiledMap` filling a region at the requested rotation. HERE BE DRAGONS! This function took a lot of trial and error to get right, so modify with CAUTION! The `proritise_tiles = True` option means that the tiling will not break up the tiles in `TileUnit`s at the boundaries between areas in the mapped region, but will instead ensure that tiles remain complete, picking up their data from the region zone which they overlap the most. The exact order in which operations are performed affects performance. For example, the final clipping to self.region when ragged_edges = False is _much_ slower if it is carried out before the dissolving of tiles into the region zones. So... again... modify CAREFULLY! Args: rotation (float, optional): An optional rotation to apply. Defaults to 0. join_on_prototiles (bool, optional): if True data from the region dataset are joined to tiles based on the prototile to which they belong. If False the join is based on the tiles in relation to the region areas. For weave-based tilings False is probably to be preferred. Defaults to True. prioritise_tiles (bool, optional): if True tiles will not be broken at boundaries in the region dataset. Defaults to True. ragged_edges (bool, optional): if True tiles at the edge of the region will not be cut by the region extent - ignored if prioritise_tiles is False when edges will always be clipped to the region extent. Defaults to True. use_centroid_lookup_approximation (bool, optional): if True use tile centroids for lookup of region data - ignored if prioritise_tiles is False when it is irrelevant. Defaults to False. debug (bool, optional): if True prints timing messages. Defaults to False. Returns: TiledMap: a TiledMap of the source region. """ if debug: t1 = perf_counter() id_var = self._setup_region_DZID() if join_on_prototiles: tiled_map, join_layer = self.rotated(rotation) tiled_map["joinUID"] = self.tiles["prototile_id"] else: tiled_map = self.rotated(rotation)[0] tiled_map["joinUID"] = self.tiles["tile_id"] join_layer = tiled_map join_layer["joinUID"] = list(range(join_layer.shape[0])) # compile a list of the variable names we are NOT going to change # i.e. everything except the geometry and the id_var region_vars = list(self.region.columns) region_vars.remove("geometry") region_vars.remove(id_var) if debug: t2 = perf_counter() print(f"STEP 1: prep data (rotation if requested): {t2 - t1:.3f}") if prioritise_tiles: # maintain tile continuity across zone boundaries # select only tiles inside a spacing buffer of the region # make column with unique ID for every tile in the tiling # the join ID is unique per tile # if join_on_prototiles: # tiled_map["joinUID"] = self.tiles["prototile_id"] # else: # tiled_map["joinUID"] = self.tiles["tile_id"] if use_centroid_lookup_approximation: t5 = perf_counter() tile_pts = copy.deepcopy(join_layer) tile_pts.geometry = tile_pts.centroid lookup = tile_pts.sjoin( self.region, how = "inner")[["joinUID", id_var]] else: # determine areas of overlapping tiles and drop the data we join the # data back later, so dropping makes that easier overlaying in region. # overlay(tiles) seems to be faster?? # TODO: also... this part is performance-critical, think about fixes -- # possibly including the above centroid-based approx overlaps = self.region.overlay(join_layer, make_valid = False) # overlaps = self.region.overlay(tiled_map, make_valid = False) if debug: t3 = perf_counter() print(f"STEP A2: overlay zones with tiling: {t3 - t2:.3f}") overlaps["area"] = overlaps.geometry.area if debug: t4 = perf_counter() print(f"STEP A3: calculate areas: {t4 - t3:.3f}") overlaps.drop(columns = region_vars, inplace = True) if debug: t5 = perf_counter() print(f"STEP A4: drop columns prior to join: {t5 - t4:.3f}") # make a lookup by largest area tile to region id lookup = overlaps \ .iloc[overlaps.groupby("joinUID")["area"] \ .agg(pd.Series.idxmax)][["joinUID", id_var]] # now join the lookup and from there the region data if debug: t6 = perf_counter() print(f"STEP A5: build lookup for join: {t6 - t5:.3f}") tiled_map = tiled_map \ .merge(lookup, on = "joinUID") \ .merge(self.region.drop(columns = ["geometry"]), on = id_var) if debug: t7 = perf_counter() print(f"STEP A6: perform lookup join: {t7 - t6:.3f}") tiled_map.drop(columns = ["joinUID"], inplace = True) else: # here we overlay tiled_map = self.region.overlay(tiled_map) t7 = perf_counter() if debug: print(f"STEP B2: overlay tiling with zones: {t7 - t2:.3f}") if join_on_prototiles: tiled_map = tiled_map.loc[ shapely.intersects(self.region_union, np.array(tiled_map.geometry)), :] tiled_map.drop(columns = [id_var], inplace = True) self.region.drop(columns = [id_var], inplace = True) # if we've retained tiles and want 'clean' edges, then clip # note that this step is slow: geopandas unary_unions the clip layer if prioritise_tiles and not ragged_edges: tiled_map.sindex tiled_map = tiled_map.clip(self.region) if debug: print(f"""STEP A7/B3: clip map to region: {perf_counter() - t7:.3f}""") tm = TiledMap() tm.tiling = self tm.map = tiled_map return tm def _setup_region_DZID(self) -> str: """Creates a new guaranteed-unique attribute in the self.region dataframe, and returns its name. Avoids a name clash with any existing attribute in the dataframe. Returns: str: name of the added attribute. """ dzid = "DZID" i = 0 while dzid in self.region.columns: dzid = "DZID" + str(i) i = i + 1 self.region[dzid] = list(range(self.region.shape[0])) return dzid def _rotate_gdf_to_geoseries( self, gdf:gpd.GeoDataFrame, angle:float, centre:tuple = (0, 0) ) -> tuple[gpd.GeoSeries, tuple[float]]: """Rotates the geometries in a GeoDataFrame as a single collection. Rotation is about the supplied centre or about the centroid of the GeoDataFrame (if not). This allows for reversal of a rotation. [Note that this might not be a required precaution!] Args: gdf (geopandas.GeoDataFrame): GeoDataFrame to rotate angle (float): angle of rotation (degrees). centre (tuple, optional): desired centre of rotation. Defaults to (0, 0). Returns: tuple: a geopandas.GeoSeries and a tuple (point) of the centre of the rotation. """ centre = ( gdf.geometry.unary_union.centroid.coords[0] if centre is None else centre) return gdf.geometry.rotate(angle, origin = centre), centre def make_tiling(self) -> gpd.GeoDataFrame: """Tiles the region with a tile unit, returning a GeoDataFrame Returns: geopandas.GeoDataFrame: a GeoDataFrame of the region tiled with the tile unit. """ # we assume the geometry column is called geometry so make it so... if self.region.geometry.name != "geometry": self.region.rename_geometry("geometry", inplace = True) # chain list of lists of GeoSeries geometries to list of geometries tiles = itertools.chain(*[ self.tile_unit.tiles.geometry.translate(p.x, p.y) for p in self.grid.points]) prototiles = itertools.chain(*[ self.tile_unit.prototile.geometry.translate(p.x, p.y) for p in self.grid.points]) # replicate the tile ids prototile_ids = list(range(len(self.grid.points))) tile_ids = list(self.tile_unit.tiles.tile_id) * len(self.grid.points) tile_prototile_ids = sorted(prototile_ids * self.tile_unit.tiles.shape[0]) tiles_gs = gpd.GeoSeries(tiles) prototiles_gs = gpd.GeoSeries(prototiles) # assemble and return as GeoDataFrames tiles_gdf = gpd.GeoDataFrame( data = {"tile_id": tile_ids, "prototile_id": tile_prototile_ids}, geometry = tiles_gs, crs = self.tile_unit.crs) prototiles_gdf = gpd.GeoDataFrame( data = {"prototile_id": prototile_ids}, geometry = prototiles_gs, crs = self.tile_unit.crs) # unclear if we need the gridify or not... return (tiling_utils.gridify(tiles_gdf), tiling_utils.gridify(prototiles_gdf)) def rotated(self, rotation:float = None) -> gpd.GeoDataFrame: """Returns the stored tiling rotated. Args: rotation (float, optional): Rotation angle in degrees. Defaults to None. Returns: gpd.GeoDataFrame: Rotated tiling. """ if self.tiles is None: self.tiles = self.make_tiling() self.rotation = rotation if self.rotation == 0: return self.tiles, self.prototiles tiles = gpd.GeoDataFrame( data = {"tile_id": self.tiles.tile_id, "prototile_id": self.tiles.tile_id}, crs = self.tiles.crs, geometry = tiling_utils.gridify( self.tiles.geometry.rotate(rotation, origin = self.grid.centre))) prototiles = gpd.GeoDataFrame( data = {"prototile_id": self.prototiles.prototile_id}, crs = self.prototiles.crs, geometry = tiling_utils.gridify( self.prototiles.geometry.rotate(rotation, origin = self.grid.centre))) return tiles, prototiles
Class that applies a
Tileable
object to a region to be mapped.The result of the tiling procedure is stored in the
tiles
variable and covers a region sufficient that the tiling can be rotated to any desired angle.Class to persist a tiling by filling an area relative to a region sufficient to apply the tiling at any rotation.
The Tiling constructor allows a number of adjustments to the supplied
Tileable
object:prototile_margin
values greater than 0 will introduce spacing of the specified distance between tiles on the boundary of each tile by applying theTileUnit.inset_prototile()
method. Note that this operation does not make sense forWeaveUnit
objects, and may not preserve the equality of tile areas.tiles_sf
values less than one scale down tiles by applying theTileUnit.scale_tiles()
method. Does not make sense forWeaveUnit
objects.tiles_margin
values greater than one apply a negative buffer of the specified distance to every tile in the tiling by applying theTileable.inset_tiles()
method. This option is applicable to bothWeaveUnit
andTileUnit
objects.
Args
unit
:Tileable
- the tile_unit to use.
region
:gpd.GeoDataFrame
- the region to be tiled.
prototile_margin
:float
, optional- values greater than 0 apply an inset margin to the tile unit. Defaults to 0.
tiles_sf
:float
, optional- scales the tiles. Defaults to 1.
tiles_margin
:float
, optional- applies a negative buffer to the tiles. Defaults to 0.
as_icons
:bool
, optional- if True prototiles will only be placed at the region's zone centroids, one per zone. Defaults to False.
Class variables
var grid : weavingspace.tile_map._TileGrid
-
the grid which will be used to apply the tiling.
var prototiles : geopandas.geodataframe.GeoDataFrame
-
the prototiles after tiling has been carried out.
var region : geopandas.geodataframe.GeoDataFrame
-
the region to be tiled.
var region_union : shapely.geometry.polygon.Polygon
-
The type of the None singleton.
var rotation : float
-
the cumulative rotation already applied to the tiling.
var tile_shape : TileShape
-
base shape of the tileable.
var tile_unit : Tileable
-
tileable on which the tiling is based.
var tiles : geopandas.geodataframe.GeoDataFrame
-
the tiles after tiling has been carried out.
Methods
def get_tiled_map(self,
rotation: float = 0.0,
join_on_prototiles: bool = True,
prioritise_tiles: bool = True,
ragged_edges: bool = True,
use_centroid_lookup_approximation=False,
debug=False) ‑> TiledMap-
Expand source code
def get_tiled_map(self, rotation:float = 0., join_on_prototiles:bool = True, prioritise_tiles:bool = True, ragged_edges:bool = True, use_centroid_lookup_approximation = False, debug = False) -> "TiledMap": """Returns a `TiledMap` filling a region at the requested rotation. HERE BE DRAGONS! This function took a lot of trial and error to get right, so modify with CAUTION! The `proritise_tiles = True` option means that the tiling will not break up the tiles in `TileUnit`s at the boundaries between areas in the mapped region, but will instead ensure that tiles remain complete, picking up their data from the region zone which they overlap the most. The exact order in which operations are performed affects performance. For example, the final clipping to self.region when ragged_edges = False is _much_ slower if it is carried out before the dissolving of tiles into the region zones. So... again... modify CAREFULLY! Args: rotation (float, optional): An optional rotation to apply. Defaults to 0. join_on_prototiles (bool, optional): if True data from the region dataset are joined to tiles based on the prototile to which they belong. If False the join is based on the tiles in relation to the region areas. For weave-based tilings False is probably to be preferred. Defaults to True. prioritise_tiles (bool, optional): if True tiles will not be broken at boundaries in the region dataset. Defaults to True. ragged_edges (bool, optional): if True tiles at the edge of the region will not be cut by the region extent - ignored if prioritise_tiles is False when edges will always be clipped to the region extent. Defaults to True. use_centroid_lookup_approximation (bool, optional): if True use tile centroids for lookup of region data - ignored if prioritise_tiles is False when it is irrelevant. Defaults to False. debug (bool, optional): if True prints timing messages. Defaults to False. Returns: TiledMap: a TiledMap of the source region. """ if debug: t1 = perf_counter() id_var = self._setup_region_DZID() if join_on_prototiles: tiled_map, join_layer = self.rotated(rotation) tiled_map["joinUID"] = self.tiles["prototile_id"] else: tiled_map = self.rotated(rotation)[0] tiled_map["joinUID"] = self.tiles["tile_id"] join_layer = tiled_map join_layer["joinUID"] = list(range(join_layer.shape[0])) # compile a list of the variable names we are NOT going to change # i.e. everything except the geometry and the id_var region_vars = list(self.region.columns) region_vars.remove("geometry") region_vars.remove(id_var) if debug: t2 = perf_counter() print(f"STEP 1: prep data (rotation if requested): {t2 - t1:.3f}") if prioritise_tiles: # maintain tile continuity across zone boundaries # select only tiles inside a spacing buffer of the region # make column with unique ID for every tile in the tiling # the join ID is unique per tile # if join_on_prototiles: # tiled_map["joinUID"] = self.tiles["prototile_id"] # else: # tiled_map["joinUID"] = self.tiles["tile_id"] if use_centroid_lookup_approximation: t5 = perf_counter() tile_pts = copy.deepcopy(join_layer) tile_pts.geometry = tile_pts.centroid lookup = tile_pts.sjoin( self.region, how = "inner")[["joinUID", id_var]] else: # determine areas of overlapping tiles and drop the data we join the # data back later, so dropping makes that easier overlaying in region. # overlay(tiles) seems to be faster?? # TODO: also... this part is performance-critical, think about fixes -- # possibly including the above centroid-based approx overlaps = self.region.overlay(join_layer, make_valid = False) # overlaps = self.region.overlay(tiled_map, make_valid = False) if debug: t3 = perf_counter() print(f"STEP A2: overlay zones with tiling: {t3 - t2:.3f}") overlaps["area"] = overlaps.geometry.area if debug: t4 = perf_counter() print(f"STEP A3: calculate areas: {t4 - t3:.3f}") overlaps.drop(columns = region_vars, inplace = True) if debug: t5 = perf_counter() print(f"STEP A4: drop columns prior to join: {t5 - t4:.3f}") # make a lookup by largest area tile to region id lookup = overlaps \ .iloc[overlaps.groupby("joinUID")["area"] \ .agg(pd.Series.idxmax)][["joinUID", id_var]] # now join the lookup and from there the region data if debug: t6 = perf_counter() print(f"STEP A5: build lookup for join: {t6 - t5:.3f}") tiled_map = tiled_map \ .merge(lookup, on = "joinUID") \ .merge(self.region.drop(columns = ["geometry"]), on = id_var) if debug: t7 = perf_counter() print(f"STEP A6: perform lookup join: {t7 - t6:.3f}") tiled_map.drop(columns = ["joinUID"], inplace = True) else: # here we overlay tiled_map = self.region.overlay(tiled_map) t7 = perf_counter() if debug: print(f"STEP B2: overlay tiling with zones: {t7 - t2:.3f}") if join_on_prototiles: tiled_map = tiled_map.loc[ shapely.intersects(self.region_union, np.array(tiled_map.geometry)), :] tiled_map.drop(columns = [id_var], inplace = True) self.region.drop(columns = [id_var], inplace = True) # if we've retained tiles and want 'clean' edges, then clip # note that this step is slow: geopandas unary_unions the clip layer if prioritise_tiles and not ragged_edges: tiled_map.sindex tiled_map = tiled_map.clip(self.region) if debug: print(f"""STEP A7/B3: clip map to region: {perf_counter() - t7:.3f}""") tm = TiledMap() tm.tiling = self tm.map = tiled_map return tm
Returns a
TiledMap
filling a region at the requested rotation.HERE BE DRAGONS! This function took a lot of trial and error to get right, so modify with CAUTION!
The
proritise_tiles = True
option means that the tiling will not break up the tiles inTileUnit
s at the boundaries between areas in the mapped region, but will instead ensure that tiles remain complete, picking up their data from the region zone which they overlap the most.The exact order in which operations are performed affects performance. For example, the final clipping to self.region when ragged_edges = False is much slower if it is carried out before the dissolving of tiles into the region zones. So… again… modify CAREFULLY!
Args
rotation
:float
, optional- An optional rotation to apply. Defaults to 0.
join_on_prototiles
:bool
, optional- if True data from the region dataset are joined to tiles based on the prototile to which they belong. If False the join is based on the tiles in relation to the region areas. For weave-based tilings False is probably to be preferred. Defaults to True.
prioritise_tiles
:bool
, optional- if True tiles will not be broken at boundaries in the region dataset. Defaults to True.
ragged_edges
:bool
, optional- if True tiles at the edge of the region will not be cut by the region extent - ignored if prioritise_tiles is False when edges will always be clipped to the region extent. Defaults to True.
use_centroid_lookup_approximation
:bool
, optional- if True use tile centroids for lookup of region data - ignored if prioritise_tiles is False when it is irrelevant. Defaults to False.
debug
:bool
, optional- if True prints timing messages. Defaults to False.
Returns
TiledMap
- a TiledMap of the source region.
def make_tiling(self) ‑> geopandas.geodataframe.GeoDataFrame
-
Expand source code
def make_tiling(self) -> gpd.GeoDataFrame: """Tiles the region with a tile unit, returning a GeoDataFrame Returns: geopandas.GeoDataFrame: a GeoDataFrame of the region tiled with the tile unit. """ # we assume the geometry column is called geometry so make it so... if self.region.geometry.name != "geometry": self.region.rename_geometry("geometry", inplace = True) # chain list of lists of GeoSeries geometries to list of geometries tiles = itertools.chain(*[ self.tile_unit.tiles.geometry.translate(p.x, p.y) for p in self.grid.points]) prototiles = itertools.chain(*[ self.tile_unit.prototile.geometry.translate(p.x, p.y) for p in self.grid.points]) # replicate the tile ids prototile_ids = list(range(len(self.grid.points))) tile_ids = list(self.tile_unit.tiles.tile_id) * len(self.grid.points) tile_prototile_ids = sorted(prototile_ids * self.tile_unit.tiles.shape[0]) tiles_gs = gpd.GeoSeries(tiles) prototiles_gs = gpd.GeoSeries(prototiles) # assemble and return as GeoDataFrames tiles_gdf = gpd.GeoDataFrame( data = {"tile_id": tile_ids, "prototile_id": tile_prototile_ids}, geometry = tiles_gs, crs = self.tile_unit.crs) prototiles_gdf = gpd.GeoDataFrame( data = {"prototile_id": prototile_ids}, geometry = prototiles_gs, crs = self.tile_unit.crs) # unclear if we need the gridify or not... return (tiling_utils.gridify(tiles_gdf), tiling_utils.gridify(prototiles_gdf))
Tiles the region with a tile unit, returning a GeoDataFrame
Returns
geopandas.GeoDataFrame
- a GeoDataFrame of the region tiled with the tile unit.
def rotated(self, rotation: float = None) ‑> geopandas.geodataframe.GeoDataFrame
-
Expand source code
def rotated(self, rotation:float = None) -> gpd.GeoDataFrame: """Returns the stored tiling rotated. Args: rotation (float, optional): Rotation angle in degrees. Defaults to None. Returns: gpd.GeoDataFrame: Rotated tiling. """ if self.tiles is None: self.tiles = self.make_tiling() self.rotation = rotation if self.rotation == 0: return self.tiles, self.prototiles tiles = gpd.GeoDataFrame( data = {"tile_id": self.tiles.tile_id, "prototile_id": self.tiles.tile_id}, crs = self.tiles.crs, geometry = tiling_utils.gridify( self.tiles.geometry.rotate(rotation, origin = self.grid.centre))) prototiles = gpd.GeoDataFrame( data = {"prototile_id": self.prototiles.prototile_id}, crs = self.prototiles.crs, geometry = tiling_utils.gridify( self.prototiles.geometry.rotate(rotation, origin = self.grid.centre))) return tiles, prototiles
Returns the stored tiling rotated.
Args
rotation
:float
, optional- Rotation angle in degrees. Defaults to None.
Returns
gpd.GeoDataFrame
- Rotated tiling.