Skip to content

Grid Module

gigaspatial.grid

CountryMercatorTiles

Bases: MercatorTiles

MercatorTiles specialized for country-level operations.

This class extends MercatorTiles to work specifically with country boundaries. It can only be instantiated through the create() classmethod.

Source code in gigaspatial/grid/mercator_tiles.py
class CountryMercatorTiles(MercatorTiles):
    """MercatorTiles specialized for country-level operations.

    This class extends MercatorTiles to work specifically with country boundaries.
    It can only be instantiated through the create() classmethod.
    """

    country: str = Field(..., exclude=True)

    def __init__(self, *args, **kwargs):
        raise TypeError(
            "CountryMercatorTiles cannot be instantiated directly. "
            "Use CountryMercatorTiles.create() instead."
        )

    @classmethod
    def create(
        cls,
        country: str,
        zoom_level: int,
        predicate: str = "intersects",
        data_store: Optional[DataStore] = None,
        country_geom_path: Optional[Union[str, Path]] = None,
    ):
        """Create CountryMercatorTiles for a specific country."""
        from gigaspatial.handlers.boundaries import AdminBoundaries

        instance = super().__new__(cls)
        super(CountryMercatorTiles, instance).__init__(
            zoom_level=zoom_level,
            quadkeys=[],
            data_store=data_store or LocalDataStore(),
            country=pycountry.countries.lookup(country).alpha_3,
        )

        country_geom = (
            AdminBoundaries.create(
                country_code=country,
                data_store=data_store,
                path=country_geom_path,
            )
            .boundaries[0]
            .geometry
        )

        tiles = MercatorTiles.from_geometry(country_geom, zoom_level, predicate)

        instance.quadkeys = tiles.quadkeys
        return instance

create(country, zoom_level, predicate='intersects', data_store=None, country_geom_path=None) classmethod

Create CountryMercatorTiles for a specific country.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def create(
    cls,
    country: str,
    zoom_level: int,
    predicate: str = "intersects",
    data_store: Optional[DataStore] = None,
    country_geom_path: Optional[Union[str, Path]] = None,
):
    """Create CountryMercatorTiles for a specific country."""
    from gigaspatial.handlers.boundaries import AdminBoundaries

    instance = super().__new__(cls)
    super(CountryMercatorTiles, instance).__init__(
        zoom_level=zoom_level,
        quadkeys=[],
        data_store=data_store or LocalDataStore(),
        country=pycountry.countries.lookup(country).alpha_3,
    )

    country_geom = (
        AdminBoundaries.create(
            country_code=country,
            data_store=data_store,
            path=country_geom_path,
        )
        .boundaries[0]
        .geometry
    )

    tiles = MercatorTiles.from_geometry(country_geom, zoom_level, predicate)

    instance.quadkeys = tiles.quadkeys
    return instance

DataStore

Bases: ABC

Abstract base class defining the interface for data store implementations. This class serves as a parent for both local and cloud-based storage solutions.

Source code in gigaspatial/core/io/data_store.py
class DataStore(ABC):
    """
    Abstract base class defining the interface for data store implementations.
    This class serves as a parent for both local and cloud-based storage solutions.
    """

    @abstractmethod
    def read_file(self, path: str) -> Any:
        """
        Read contents of a file from the data store.

        Args:
            path: Path to the file to read

        Returns:
            Contents of the file

        Raises:
            IOError: If file cannot be read
        """
        pass

    @abstractmethod
    def write_file(self, path: str, data: Any) -> None:
        """
        Write data to a file in the data store.

        Args:
            path: Path where to write the file
            data: Data to write to the file

        Raises:
            IOError: If file cannot be written
        """
        pass

    @abstractmethod
    def file_exists(self, path: str) -> bool:
        """
        Check if a file exists in the data store.

        Args:
            path: Path to check

        Returns:
            True if file exists, False otherwise
        """
        pass

    @abstractmethod
    def list_files(self, path: str) -> List[str]:
        """
        List all files in a directory.

        Args:
            path: Directory path to list

        Returns:
            List of file paths in the directory
        """
        pass

    @abstractmethod
    def walk(self, top: str) -> Generator:
        """
        Walk through directory tree, similar to os.walk().

        Args:
            top: Starting directory for the walk

        Returns:
            Generator yielding tuples of (dirpath, dirnames, filenames)
        """
        pass

    @abstractmethod
    def open(self, file: str, mode: str = "r") -> Union[str, bytes]:
        """
        Context manager for file operations.

        Args:
            file: Path to the file
            mode: File mode ('r', 'w', 'rb', 'wb')

        Yields:
            File-like object

        Raises:
            IOError: If file cannot be opened
        """
        pass

    @abstractmethod
    def is_file(self, path: str) -> bool:
        """
        Check if path points to a file.

        Args:
            path: Path to check

        Returns:
            True if path is a file, False otherwise
        """
        pass

    @abstractmethod
    def is_dir(self, path: str) -> bool:
        """
        Check if path points to a directory.

        Args:
            path: Path to check

        Returns:
            True if path is a directory, False otherwise
        """
        pass

    @abstractmethod
    def remove(self, path: str) -> None:
        """
        Remove a file.

        Args:
            path: Path to the file to remove

        Raises:
            IOError: If file cannot be removed
        """
        pass

    @abstractmethod
    def rmdir(self, dir: str) -> None:
        """
        Remove a directory and all its contents.

        Args:
            dir: Path to the directory to remove

        Raises:
            IOError: If directory cannot be removed
        """
        pass

file_exists(path) abstractmethod

Check if a file exists in the data store.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if file exists, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def file_exists(self, path: str) -> bool:
    """
    Check if a file exists in the data store.

    Args:
        path: Path to check

    Returns:
        True if file exists, False otherwise
    """
    pass

is_dir(path) abstractmethod

Check if path points to a directory.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if path is a directory, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def is_dir(self, path: str) -> bool:
    """
    Check if path points to a directory.

    Args:
        path: Path to check

    Returns:
        True if path is a directory, False otherwise
    """
    pass

is_file(path) abstractmethod

Check if path points to a file.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if path is a file, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def is_file(self, path: str) -> bool:
    """
    Check if path points to a file.

    Args:
        path: Path to check

    Returns:
        True if path is a file, False otherwise
    """
    pass

list_files(path) abstractmethod

List all files in a directory.

Parameters:

Name Type Description Default
path str

Directory path to list

required

Returns:

Type Description
List[str]

List of file paths in the directory

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def list_files(self, path: str) -> List[str]:
    """
    List all files in a directory.

    Args:
        path: Directory path to list

    Returns:
        List of file paths in the directory
    """
    pass

open(file, mode='r') abstractmethod

Context manager for file operations.

Parameters:

Name Type Description Default
file str

Path to the file

required
mode str

File mode ('r', 'w', 'rb', 'wb')

'r'

Yields:

Type Description
Union[str, bytes]

File-like object

Raises:

Type Description
IOError

If file cannot be opened

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def open(self, file: str, mode: str = "r") -> Union[str, bytes]:
    """
    Context manager for file operations.

    Args:
        file: Path to the file
        mode: File mode ('r', 'w', 'rb', 'wb')

    Yields:
        File-like object

    Raises:
        IOError: If file cannot be opened
    """
    pass

read_file(path) abstractmethod

Read contents of a file from the data store.

Parameters:

Name Type Description Default
path str

Path to the file to read

required

Returns:

Type Description
Any

Contents of the file

Raises:

Type Description
IOError

If file cannot be read

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def read_file(self, path: str) -> Any:
    """
    Read contents of a file from the data store.

    Args:
        path: Path to the file to read

    Returns:
        Contents of the file

    Raises:
        IOError: If file cannot be read
    """
    pass

remove(path) abstractmethod

Remove a file.

Parameters:

Name Type Description Default
path str

Path to the file to remove

required

Raises:

Type Description
IOError

If file cannot be removed

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def remove(self, path: str) -> None:
    """
    Remove a file.

    Args:
        path: Path to the file to remove

    Raises:
        IOError: If file cannot be removed
    """
    pass

rmdir(dir) abstractmethod

Remove a directory and all its contents.

Parameters:

Name Type Description Default
dir str

Path to the directory to remove

required

Raises:

Type Description
IOError

If directory cannot be removed

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def rmdir(self, dir: str) -> None:
    """
    Remove a directory and all its contents.

    Args:
        dir: Path to the directory to remove

    Raises:
        IOError: If directory cannot be removed
    """
    pass

walk(top) abstractmethod

Walk through directory tree, similar to os.walk().

Parameters:

Name Type Description Default
top str

Starting directory for the walk

required

Returns:

Type Description
Generator

Generator yielding tuples of (dirpath, dirnames, filenames)

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def walk(self, top: str) -> Generator:
    """
    Walk through directory tree, similar to os.walk().

    Args:
        top: Starting directory for the walk

    Returns:
        Generator yielding tuples of (dirpath, dirnames, filenames)
    """
    pass

write_file(path, data) abstractmethod

Write data to a file in the data store.

Parameters:

Name Type Description Default
path str

Path where to write the file

required
data Any

Data to write to the file

required

Raises:

Type Description
IOError

If file cannot be written

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def write_file(self, path: str, data: Any) -> None:
    """
    Write data to a file in the data store.

    Args:
        path: Path where to write the file
        data: Data to write to the file

    Raises:
        IOError: If file cannot be written
    """
    pass

LocalDataStore

Bases: DataStore

Implementation for local filesystem storage.

Source code in gigaspatial/core/io/local_data_store.py
class LocalDataStore(DataStore):
    """Implementation for local filesystem storage."""

    def __init__(self, base_path: Union[str, Path] = ""):
        super().__init__()
        self.base_path = Path(base_path).resolve()

    def _resolve_path(self, path: str) -> Path:
        """Resolve path relative to base directory."""
        return self.base_path / path

    def read_file(self, path: str) -> bytes:
        full_path = self._resolve_path(path)
        with open(full_path, "rb") as f:
            return f.read()

    def write_file(self, path: str, data: Union[bytes, str]) -> None:
        full_path = self._resolve_path(path)
        self.mkdir(str(full_path.parent), exist_ok=True)

        if isinstance(data, str):
            mode = "w"
            encoding = "utf-8"
        else:
            mode = "wb"
            encoding = None

        with open(full_path, mode, encoding=encoding) as f:
            f.write(data)

    def file_exists(self, path: str) -> bool:
        return self._resolve_path(path).is_file()

    def list_files(self, path: str) -> List[str]:
        full_path = self._resolve_path(path)
        return [
            str(f.relative_to(self.base_path))
            for f in full_path.iterdir()
            if f.is_file()
        ]

    def walk(self, top: str) -> Generator[Tuple[str, List[str], List[str]], None, None]:
        full_path = self._resolve_path(top)
        for root, dirs, files in os.walk(full_path):
            rel_root = str(Path(root).relative_to(self.base_path))
            yield rel_root, dirs, files

    def list_directories(self, path: str) -> List[str]:
        full_path = self._resolve_path(path)

        if not full_path.exists():
            return []

        if not full_path.is_dir():
            return []

        return [d.name for d in full_path.iterdir() if d.is_dir()]

    def open(self, path: str, mode: str = "r") -> IO:
        full_path = self._resolve_path(path)
        self.mkdir(str(full_path.parent), exist_ok=True)
        return open(full_path, mode)

    def is_file(self, path: str) -> bool:
        return self._resolve_path(path).is_file()

    def is_dir(self, path: str) -> bool:
        return self._resolve_path(path).is_dir()

    def remove(self, path: str) -> None:
        full_path = self._resolve_path(path)
        if full_path.is_file():
            os.remove(full_path)

    def rmdir(self, directory: str) -> None:
        full_path = self._resolve_path(directory)
        if full_path.is_dir():
            os.rmdir(full_path)

    def mkdir(self, path: str, exist_ok: bool = False) -> None:
        full_path = self._resolve_path(path)
        full_path.mkdir(parents=True, exist_ok=exist_ok)

    def exists(self, path: str) -> bool:
        return self._resolve_path(path).exists()

MercatorTiles

Bases: BaseModel

Source code in gigaspatial/grid/mercator_tiles.py
class MercatorTiles(BaseModel):
    zoom_level: int = Field(..., ge=0, le=20)
    quadkeys: List[str] = Field(default_factory=list)
    data_store: DataStore = Field(default_factory=LocalDataStore, exclude=True)
    logger: ClassVar = config.get_logger("MercatorTiles")

    class Config:
        arbitrary_types_allowed = True

    @classmethod
    def from_quadkeys(cls, quadkeys: List[str]):
        """Create MercatorTiles from list of quadkeys."""
        if not quadkeys:
            cls.logger.warning("No quadkeys provided to from_quadkeys.")
            return cls(zoom_level=0, quadkeys=[])
        return cls(zoom_level=len(quadkeys[0]), quadkeys=set(quadkeys))

    @classmethod
    def from_bounds(
        cls, xmin: float, ymin: float, xmax: float, ymax: float, zoom_level: int
    ):
        """Create MercatorTiles from boundary coordinates."""
        cls.logger.info(
            f"Creating MercatorTiles from bounds: ({xmin}, {ymin}, {xmax}, {ymax}) at zoom level: {zoom_level}"
        )
        return cls(
            zoom_level=zoom_level,
            quadkeys=[
                mercantile.quadkey(tile)
                for tile in mercantile.tiles(xmin, ymin, xmax, ymax, zoom_level)
            ],
        )

    @classmethod
    def from_spatial(
        cls,
        source: Union[
            BaseGeometry,
            gpd.GeoDataFrame,
            List[Union[Point, Tuple[float, float]]],  # points
        ],
        zoom_level: int,
        predicate: str = "intersects",
        **kwargs,
    ):
        cls.logger.info(
            f"Creating MercatorTiles from spatial source (type: {type(source)}) at zoom level: {zoom_level} with predicate: {predicate}"
        )
        if isinstance(source, gpd.GeoDataFrame):
            if source.crs != "EPSG:4326":
                source = source.to_crs("EPSG:4326")
            source = source.geometry.unary_union

        if isinstance(source, BaseGeometry):
            return cls.from_geometry(
                geometry=source, zoom_level=zoom_level, predicate=predicate, **kwargs
            )
        elif isinstance(source, Iterable) and all(
            len(pt) == 2 or isinstance(pt, Point) for pt in source
        ):
            return cls.from_points(geometry=source, zoom_level=zoom_level, **kwargs)
        else:
            raise

    @classmethod
    def from_geometry(
        cls,
        geometry: BaseGeometry,
        zoom_level: int,
        predicate: str = "intersects",
        **kwargs,
    ):
        """Create MercatorTiles from a polygon."""
        cls.logger.info(
            f"Creating MercatorTiles from geometry (bounds: {geometry.bounds}) at zoom level: {zoom_level} with predicate: {predicate}"
        )
        tiles = list(mercantile.tiles(*geometry.bounds, zoom_level))
        quadkeys_boxes = [
            (mercantile.quadkey(t), box(*mercantile.bounds(t))) for t in tiles
        ]
        quadkeys, boxes = zip(*quadkeys_boxes) if quadkeys_boxes else ([], [])

        if not boxes:
            cls.logger.warning(
                "No boxes generated from geometry bounds. Returning empty MercatorTiles."
            )
            return MercatorTiles(zoom_level=zoom_level, quadkeys=[])

        s = STRtree(boxes)
        result_indices = s.query(geometry, predicate=predicate)
        filtered_quadkeys = [quadkeys[i] for i in result_indices]
        cls.logger.info(
            f"Filtered down to {len(filtered_quadkeys)} quadkeys using spatial predicate."
        )
        return cls(zoom_level=zoom_level, quadkeys=filtered_quadkeys, **kwargs)

    @classmethod
    def from_points(
        cls, points: List[Union[Point, Tuple[float, float]]], zoom_level: int, **kwargs
    ) -> "MercatorTiles":
        """Create MercatorTiles from a list of points or lat-lon pairs."""
        cls.logger.info(
            f"Creating MercatorTiles from {len(points)} points at zoom level: {zoom_level}"
        )
        quadkeys = {
            (
                mercantile.quadkey(mercantile.tile(p.x, p.y, zoom_level))
                if isinstance(p, Point)
                else mercantile.quadkey(mercantile.tile(p[1], p[0], zoom_level))
            )
            for p in points
        }
        cls.logger.info(f"Generated {len(quadkeys)} unique quadkeys from points.")
        return cls(zoom_level=zoom_level, quadkeys=list(quadkeys), **kwargs)

    @classmethod
    def from_json(
        cls, data_store: DataStore, file: Union[str, Path], **kwargs
    ) -> "MercatorTiles":
        """Load MercatorTiles from a JSON file."""
        cls.logger.info(
            f"Loading MercatorTiles from JSON file: {file} using data store: {type(data_store).__name__}"
        )
        with data_store.open(str(file), "r") as f:
            data = json.load(f)
            if isinstance(data, list):  # If file contains only quadkeys
                data = {
                    "zoom_level": len(data[0]) if data else 0,
                    "quadkeys": data,
                    **kwargs,
                }
            else:
                data.update(kwargs)
            instance = cls(**data)
            instance.data_store = data_store
            cls.logger.info(
                f"Successfully loaded {len(instance.quadkeys)} quadkeys from JSON file."
            )
            return instance

    def filter_quadkeys(self, quadkeys: Iterable[str]) -> "MercatorTiles":
        """Filter quadkeys by a given set of quadkeys."""
        original_count = len(self.quadkeys)
        incoming_count = len(
            list(quadkeys)
        )  # Convert to list to get length if it's an iterator

        self.logger.info(
            f"Filtering {original_count} quadkeys with an incoming set of {incoming_count} quadkeys."
        )
        filtered_quadkeys = list(set(self.quadkeys) & set(quadkeys))
        self.logger.info(f"Resulting in {len(filtered_quadkeys)} filtered quadkeys.")
        return MercatorTiles(
            zoom_level=self.zoom_level,
            quadkeys=filtered_quadkeys,
        )

    def to_dataframe(self) -> pd.DataFrame:
        """Convert to pandas DataFrame with quadkey and centroid coordinates."""
        self.logger.info(
            f"Converting {len(self.quadkeys)} quadkeys to pandas DataFrame."
        )
        if not self.quadkeys:
            self.logger.warning(
                "No quadkeys to convert to DataFrame. Returning empty DataFrame."
            )
            return pd.DataFrame(columns=["quadkey", "latitude", "longitude"])
        tiles_data = [mercantile.quadkey_to_tile(q) for q in self.quadkeys]
        bounds_data = [mercantile.bounds(tile) for tile in tiles_data]

        centroids = [
            (
                (bounds.south + bounds.north) / 2,  # latitude
                (bounds.west + bounds.east) / 2,  # longitude
            )
            for bounds in bounds_data
        ]

        self.logger.info(f"Successfully converted to DataFrame.")

        return pd.DataFrame(
            {
                "quadkey": self.quadkeys,
                "latitude": [c[0] for c in centroids],
                "longitude": [c[1] for c in centroids],
            }
        )

    def to_geoms(self) -> List[box]:
        self.logger.info(
            f"Converting {len(self.quadkeys)} quadkeys to shapely box geometries."
        )
        return [
            box(*mercantile.bounds(mercantile.quadkey_to_tile(q)))
            for q in self.quadkeys
        ]

    def to_geodataframe(self) -> gpd.GeoDataFrame:
        """Convert to GeoPandas GeoDataFrame."""
        return gpd.GeoDataFrame(
            {"quadkey": self.quadkeys, "geometry": self.to_geoms()}, crs="EPSG:4326"
        )

    def save(self, file: Union[str, Path], format: str = "json") -> None:
        """Save MercatorTiles to file in specified format."""
        with self.data_store.open(str(file), "wb" if format == "parquet" else "w") as f:
            if format == "parquet":
                self.to_geodataframe().to_parquet(f, index=False)
            elif format == "geojson":
                f.write(self.to_geodataframe().to_json(drop_id=True))
            elif format == "json":
                json.dump(self.quadkeys, f)
            else:
                raise ValueError(f"Unsupported format: {format}")

    def __len__(self) -> int:
        return len(self.quadkeys)

filter_quadkeys(quadkeys)

Filter quadkeys by a given set of quadkeys.

Source code in gigaspatial/grid/mercator_tiles.py
def filter_quadkeys(self, quadkeys: Iterable[str]) -> "MercatorTiles":
    """Filter quadkeys by a given set of quadkeys."""
    original_count = len(self.quadkeys)
    incoming_count = len(
        list(quadkeys)
    )  # Convert to list to get length if it's an iterator

    self.logger.info(
        f"Filtering {original_count} quadkeys with an incoming set of {incoming_count} quadkeys."
    )
    filtered_quadkeys = list(set(self.quadkeys) & set(quadkeys))
    self.logger.info(f"Resulting in {len(filtered_quadkeys)} filtered quadkeys.")
    return MercatorTiles(
        zoom_level=self.zoom_level,
        quadkeys=filtered_quadkeys,
    )

from_bounds(xmin, ymin, xmax, ymax, zoom_level) classmethod

Create MercatorTiles from boundary coordinates.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_bounds(
    cls, xmin: float, ymin: float, xmax: float, ymax: float, zoom_level: int
):
    """Create MercatorTiles from boundary coordinates."""
    cls.logger.info(
        f"Creating MercatorTiles from bounds: ({xmin}, {ymin}, {xmax}, {ymax}) at zoom level: {zoom_level}"
    )
    return cls(
        zoom_level=zoom_level,
        quadkeys=[
            mercantile.quadkey(tile)
            for tile in mercantile.tiles(xmin, ymin, xmax, ymax, zoom_level)
        ],
    )

from_geometry(geometry, zoom_level, predicate='intersects', **kwargs) classmethod

Create MercatorTiles from a polygon.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_geometry(
    cls,
    geometry: BaseGeometry,
    zoom_level: int,
    predicate: str = "intersects",
    **kwargs,
):
    """Create MercatorTiles from a polygon."""
    cls.logger.info(
        f"Creating MercatorTiles from geometry (bounds: {geometry.bounds}) at zoom level: {zoom_level} with predicate: {predicate}"
    )
    tiles = list(mercantile.tiles(*geometry.bounds, zoom_level))
    quadkeys_boxes = [
        (mercantile.quadkey(t), box(*mercantile.bounds(t))) for t in tiles
    ]
    quadkeys, boxes = zip(*quadkeys_boxes) if quadkeys_boxes else ([], [])

    if not boxes:
        cls.logger.warning(
            "No boxes generated from geometry bounds. Returning empty MercatorTiles."
        )
        return MercatorTiles(zoom_level=zoom_level, quadkeys=[])

    s = STRtree(boxes)
    result_indices = s.query(geometry, predicate=predicate)
    filtered_quadkeys = [quadkeys[i] for i in result_indices]
    cls.logger.info(
        f"Filtered down to {len(filtered_quadkeys)} quadkeys using spatial predicate."
    )
    return cls(zoom_level=zoom_level, quadkeys=filtered_quadkeys, **kwargs)

from_json(data_store, file, **kwargs) classmethod

Load MercatorTiles from a JSON file.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_json(
    cls, data_store: DataStore, file: Union[str, Path], **kwargs
) -> "MercatorTiles":
    """Load MercatorTiles from a JSON file."""
    cls.logger.info(
        f"Loading MercatorTiles from JSON file: {file} using data store: {type(data_store).__name__}"
    )
    with data_store.open(str(file), "r") as f:
        data = json.load(f)
        if isinstance(data, list):  # If file contains only quadkeys
            data = {
                "zoom_level": len(data[0]) if data else 0,
                "quadkeys": data,
                **kwargs,
            }
        else:
            data.update(kwargs)
        instance = cls(**data)
        instance.data_store = data_store
        cls.logger.info(
            f"Successfully loaded {len(instance.quadkeys)} quadkeys from JSON file."
        )
        return instance

from_points(points, zoom_level, **kwargs) classmethod

Create MercatorTiles from a list of points or lat-lon pairs.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_points(
    cls, points: List[Union[Point, Tuple[float, float]]], zoom_level: int, **kwargs
) -> "MercatorTiles":
    """Create MercatorTiles from a list of points or lat-lon pairs."""
    cls.logger.info(
        f"Creating MercatorTiles from {len(points)} points at zoom level: {zoom_level}"
    )
    quadkeys = {
        (
            mercantile.quadkey(mercantile.tile(p.x, p.y, zoom_level))
            if isinstance(p, Point)
            else mercantile.quadkey(mercantile.tile(p[1], p[0], zoom_level))
        )
        for p in points
    }
    cls.logger.info(f"Generated {len(quadkeys)} unique quadkeys from points.")
    return cls(zoom_level=zoom_level, quadkeys=list(quadkeys), **kwargs)

from_quadkeys(quadkeys) classmethod

Create MercatorTiles from list of quadkeys.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_quadkeys(cls, quadkeys: List[str]):
    """Create MercatorTiles from list of quadkeys."""
    if not quadkeys:
        cls.logger.warning("No quadkeys provided to from_quadkeys.")
        return cls(zoom_level=0, quadkeys=[])
    return cls(zoom_level=len(quadkeys[0]), quadkeys=set(quadkeys))

save(file, format='json')

Save MercatorTiles to file in specified format.

Source code in gigaspatial/grid/mercator_tiles.py
def save(self, file: Union[str, Path], format: str = "json") -> None:
    """Save MercatorTiles to file in specified format."""
    with self.data_store.open(str(file), "wb" if format == "parquet" else "w") as f:
        if format == "parquet":
            self.to_geodataframe().to_parquet(f, index=False)
        elif format == "geojson":
            f.write(self.to_geodataframe().to_json(drop_id=True))
        elif format == "json":
            json.dump(self.quadkeys, f)
        else:
            raise ValueError(f"Unsupported format: {format}")

to_dataframe()

Convert to pandas DataFrame with quadkey and centroid coordinates.

Source code in gigaspatial/grid/mercator_tiles.py
def to_dataframe(self) -> pd.DataFrame:
    """Convert to pandas DataFrame with quadkey and centroid coordinates."""
    self.logger.info(
        f"Converting {len(self.quadkeys)} quadkeys to pandas DataFrame."
    )
    if not self.quadkeys:
        self.logger.warning(
            "No quadkeys to convert to DataFrame. Returning empty DataFrame."
        )
        return pd.DataFrame(columns=["quadkey", "latitude", "longitude"])
    tiles_data = [mercantile.quadkey_to_tile(q) for q in self.quadkeys]
    bounds_data = [mercantile.bounds(tile) for tile in tiles_data]

    centroids = [
        (
            (bounds.south + bounds.north) / 2,  # latitude
            (bounds.west + bounds.east) / 2,  # longitude
        )
        for bounds in bounds_data
    ]

    self.logger.info(f"Successfully converted to DataFrame.")

    return pd.DataFrame(
        {
            "quadkey": self.quadkeys,
            "latitude": [c[0] for c in centroids],
            "longitude": [c[1] for c in centroids],
        }
    )

to_geodataframe()

Convert to GeoPandas GeoDataFrame.

Source code in gigaspatial/grid/mercator_tiles.py
def to_geodataframe(self) -> gpd.GeoDataFrame:
    """Convert to GeoPandas GeoDataFrame."""
    return gpd.GeoDataFrame(
        {"quadkey": self.quadkeys, "geometry": self.to_geoms()}, crs="EPSG:4326"
    )

mercator_tiles

CountryMercatorTiles

Bases: MercatorTiles

MercatorTiles specialized for country-level operations.

This class extends MercatorTiles to work specifically with country boundaries. It can only be instantiated through the create() classmethod.

Source code in gigaspatial/grid/mercator_tiles.py
class CountryMercatorTiles(MercatorTiles):
    """MercatorTiles specialized for country-level operations.

    This class extends MercatorTiles to work specifically with country boundaries.
    It can only be instantiated through the create() classmethod.
    """

    country: str = Field(..., exclude=True)

    def __init__(self, *args, **kwargs):
        raise TypeError(
            "CountryMercatorTiles cannot be instantiated directly. "
            "Use CountryMercatorTiles.create() instead."
        )

    @classmethod
    def create(
        cls,
        country: str,
        zoom_level: int,
        predicate: str = "intersects",
        data_store: Optional[DataStore] = None,
        country_geom_path: Optional[Union[str, Path]] = None,
    ):
        """Create CountryMercatorTiles for a specific country."""
        from gigaspatial.handlers.boundaries import AdminBoundaries

        instance = super().__new__(cls)
        super(CountryMercatorTiles, instance).__init__(
            zoom_level=zoom_level,
            quadkeys=[],
            data_store=data_store or LocalDataStore(),
            country=pycountry.countries.lookup(country).alpha_3,
        )

        country_geom = (
            AdminBoundaries.create(
                country_code=country,
                data_store=data_store,
                path=country_geom_path,
            )
            .boundaries[0]
            .geometry
        )

        tiles = MercatorTiles.from_geometry(country_geom, zoom_level, predicate)

        instance.quadkeys = tiles.quadkeys
        return instance
create(country, zoom_level, predicate='intersects', data_store=None, country_geom_path=None) classmethod

Create CountryMercatorTiles for a specific country.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def create(
    cls,
    country: str,
    zoom_level: int,
    predicate: str = "intersects",
    data_store: Optional[DataStore] = None,
    country_geom_path: Optional[Union[str, Path]] = None,
):
    """Create CountryMercatorTiles for a specific country."""
    from gigaspatial.handlers.boundaries import AdminBoundaries

    instance = super().__new__(cls)
    super(CountryMercatorTiles, instance).__init__(
        zoom_level=zoom_level,
        quadkeys=[],
        data_store=data_store or LocalDataStore(),
        country=pycountry.countries.lookup(country).alpha_3,
    )

    country_geom = (
        AdminBoundaries.create(
            country_code=country,
            data_store=data_store,
            path=country_geom_path,
        )
        .boundaries[0]
        .geometry
    )

    tiles = MercatorTiles.from_geometry(country_geom, zoom_level, predicate)

    instance.quadkeys = tiles.quadkeys
    return instance

MercatorTiles

Bases: BaseModel

Source code in gigaspatial/grid/mercator_tiles.py
class MercatorTiles(BaseModel):
    zoom_level: int = Field(..., ge=0, le=20)
    quadkeys: List[str] = Field(default_factory=list)
    data_store: DataStore = Field(default_factory=LocalDataStore, exclude=True)
    logger: ClassVar = config.get_logger("MercatorTiles")

    class Config:
        arbitrary_types_allowed = True

    @classmethod
    def from_quadkeys(cls, quadkeys: List[str]):
        """Create MercatorTiles from list of quadkeys."""
        if not quadkeys:
            cls.logger.warning("No quadkeys provided to from_quadkeys.")
            return cls(zoom_level=0, quadkeys=[])
        return cls(zoom_level=len(quadkeys[0]), quadkeys=set(quadkeys))

    @classmethod
    def from_bounds(
        cls, xmin: float, ymin: float, xmax: float, ymax: float, zoom_level: int
    ):
        """Create MercatorTiles from boundary coordinates."""
        cls.logger.info(
            f"Creating MercatorTiles from bounds: ({xmin}, {ymin}, {xmax}, {ymax}) at zoom level: {zoom_level}"
        )
        return cls(
            zoom_level=zoom_level,
            quadkeys=[
                mercantile.quadkey(tile)
                for tile in mercantile.tiles(xmin, ymin, xmax, ymax, zoom_level)
            ],
        )

    @classmethod
    def from_spatial(
        cls,
        source: Union[
            BaseGeometry,
            gpd.GeoDataFrame,
            List[Union[Point, Tuple[float, float]]],  # points
        ],
        zoom_level: int,
        predicate: str = "intersects",
        **kwargs,
    ):
        cls.logger.info(
            f"Creating MercatorTiles from spatial source (type: {type(source)}) at zoom level: {zoom_level} with predicate: {predicate}"
        )
        if isinstance(source, gpd.GeoDataFrame):
            if source.crs != "EPSG:4326":
                source = source.to_crs("EPSG:4326")
            source = source.geometry.unary_union

        if isinstance(source, BaseGeometry):
            return cls.from_geometry(
                geometry=source, zoom_level=zoom_level, predicate=predicate, **kwargs
            )
        elif isinstance(source, Iterable) and all(
            len(pt) == 2 or isinstance(pt, Point) for pt in source
        ):
            return cls.from_points(geometry=source, zoom_level=zoom_level, **kwargs)
        else:
            raise

    @classmethod
    def from_geometry(
        cls,
        geometry: BaseGeometry,
        zoom_level: int,
        predicate: str = "intersects",
        **kwargs,
    ):
        """Create MercatorTiles from a polygon."""
        cls.logger.info(
            f"Creating MercatorTiles from geometry (bounds: {geometry.bounds}) at zoom level: {zoom_level} with predicate: {predicate}"
        )
        tiles = list(mercantile.tiles(*geometry.bounds, zoom_level))
        quadkeys_boxes = [
            (mercantile.quadkey(t), box(*mercantile.bounds(t))) for t in tiles
        ]
        quadkeys, boxes = zip(*quadkeys_boxes) if quadkeys_boxes else ([], [])

        if not boxes:
            cls.logger.warning(
                "No boxes generated from geometry bounds. Returning empty MercatorTiles."
            )
            return MercatorTiles(zoom_level=zoom_level, quadkeys=[])

        s = STRtree(boxes)
        result_indices = s.query(geometry, predicate=predicate)
        filtered_quadkeys = [quadkeys[i] for i in result_indices]
        cls.logger.info(
            f"Filtered down to {len(filtered_quadkeys)} quadkeys using spatial predicate."
        )
        return cls(zoom_level=zoom_level, quadkeys=filtered_quadkeys, **kwargs)

    @classmethod
    def from_points(
        cls, points: List[Union[Point, Tuple[float, float]]], zoom_level: int, **kwargs
    ) -> "MercatorTiles":
        """Create MercatorTiles from a list of points or lat-lon pairs."""
        cls.logger.info(
            f"Creating MercatorTiles from {len(points)} points at zoom level: {zoom_level}"
        )
        quadkeys = {
            (
                mercantile.quadkey(mercantile.tile(p.x, p.y, zoom_level))
                if isinstance(p, Point)
                else mercantile.quadkey(mercantile.tile(p[1], p[0], zoom_level))
            )
            for p in points
        }
        cls.logger.info(f"Generated {len(quadkeys)} unique quadkeys from points.")
        return cls(zoom_level=zoom_level, quadkeys=list(quadkeys), **kwargs)

    @classmethod
    def from_json(
        cls, data_store: DataStore, file: Union[str, Path], **kwargs
    ) -> "MercatorTiles":
        """Load MercatorTiles from a JSON file."""
        cls.logger.info(
            f"Loading MercatorTiles from JSON file: {file} using data store: {type(data_store).__name__}"
        )
        with data_store.open(str(file), "r") as f:
            data = json.load(f)
            if isinstance(data, list):  # If file contains only quadkeys
                data = {
                    "zoom_level": len(data[0]) if data else 0,
                    "quadkeys": data,
                    **kwargs,
                }
            else:
                data.update(kwargs)
            instance = cls(**data)
            instance.data_store = data_store
            cls.logger.info(
                f"Successfully loaded {len(instance.quadkeys)} quadkeys from JSON file."
            )
            return instance

    def filter_quadkeys(self, quadkeys: Iterable[str]) -> "MercatorTiles":
        """Filter quadkeys by a given set of quadkeys."""
        original_count = len(self.quadkeys)
        incoming_count = len(
            list(quadkeys)
        )  # Convert to list to get length if it's an iterator

        self.logger.info(
            f"Filtering {original_count} quadkeys with an incoming set of {incoming_count} quadkeys."
        )
        filtered_quadkeys = list(set(self.quadkeys) & set(quadkeys))
        self.logger.info(f"Resulting in {len(filtered_quadkeys)} filtered quadkeys.")
        return MercatorTiles(
            zoom_level=self.zoom_level,
            quadkeys=filtered_quadkeys,
        )

    def to_dataframe(self) -> pd.DataFrame:
        """Convert to pandas DataFrame with quadkey and centroid coordinates."""
        self.logger.info(
            f"Converting {len(self.quadkeys)} quadkeys to pandas DataFrame."
        )
        if not self.quadkeys:
            self.logger.warning(
                "No quadkeys to convert to DataFrame. Returning empty DataFrame."
            )
            return pd.DataFrame(columns=["quadkey", "latitude", "longitude"])
        tiles_data = [mercantile.quadkey_to_tile(q) for q in self.quadkeys]
        bounds_data = [mercantile.bounds(tile) for tile in tiles_data]

        centroids = [
            (
                (bounds.south + bounds.north) / 2,  # latitude
                (bounds.west + bounds.east) / 2,  # longitude
            )
            for bounds in bounds_data
        ]

        self.logger.info(f"Successfully converted to DataFrame.")

        return pd.DataFrame(
            {
                "quadkey": self.quadkeys,
                "latitude": [c[0] for c in centroids],
                "longitude": [c[1] for c in centroids],
            }
        )

    def to_geoms(self) -> List[box]:
        self.logger.info(
            f"Converting {len(self.quadkeys)} quadkeys to shapely box geometries."
        )
        return [
            box(*mercantile.bounds(mercantile.quadkey_to_tile(q)))
            for q in self.quadkeys
        ]

    def to_geodataframe(self) -> gpd.GeoDataFrame:
        """Convert to GeoPandas GeoDataFrame."""
        return gpd.GeoDataFrame(
            {"quadkey": self.quadkeys, "geometry": self.to_geoms()}, crs="EPSG:4326"
        )

    def save(self, file: Union[str, Path], format: str = "json") -> None:
        """Save MercatorTiles to file in specified format."""
        with self.data_store.open(str(file), "wb" if format == "parquet" else "w") as f:
            if format == "parquet":
                self.to_geodataframe().to_parquet(f, index=False)
            elif format == "geojson":
                f.write(self.to_geodataframe().to_json(drop_id=True))
            elif format == "json":
                json.dump(self.quadkeys, f)
            else:
                raise ValueError(f"Unsupported format: {format}")

    def __len__(self) -> int:
        return len(self.quadkeys)
filter_quadkeys(quadkeys)

Filter quadkeys by a given set of quadkeys.

Source code in gigaspatial/grid/mercator_tiles.py
def filter_quadkeys(self, quadkeys: Iterable[str]) -> "MercatorTiles":
    """Filter quadkeys by a given set of quadkeys."""
    original_count = len(self.quadkeys)
    incoming_count = len(
        list(quadkeys)
    )  # Convert to list to get length if it's an iterator

    self.logger.info(
        f"Filtering {original_count} quadkeys with an incoming set of {incoming_count} quadkeys."
    )
    filtered_quadkeys = list(set(self.quadkeys) & set(quadkeys))
    self.logger.info(f"Resulting in {len(filtered_quadkeys)} filtered quadkeys.")
    return MercatorTiles(
        zoom_level=self.zoom_level,
        quadkeys=filtered_quadkeys,
    )
from_bounds(xmin, ymin, xmax, ymax, zoom_level) classmethod

Create MercatorTiles from boundary coordinates.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_bounds(
    cls, xmin: float, ymin: float, xmax: float, ymax: float, zoom_level: int
):
    """Create MercatorTiles from boundary coordinates."""
    cls.logger.info(
        f"Creating MercatorTiles from bounds: ({xmin}, {ymin}, {xmax}, {ymax}) at zoom level: {zoom_level}"
    )
    return cls(
        zoom_level=zoom_level,
        quadkeys=[
            mercantile.quadkey(tile)
            for tile in mercantile.tiles(xmin, ymin, xmax, ymax, zoom_level)
        ],
    )
from_geometry(geometry, zoom_level, predicate='intersects', **kwargs) classmethod

Create MercatorTiles from a polygon.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_geometry(
    cls,
    geometry: BaseGeometry,
    zoom_level: int,
    predicate: str = "intersects",
    **kwargs,
):
    """Create MercatorTiles from a polygon."""
    cls.logger.info(
        f"Creating MercatorTiles from geometry (bounds: {geometry.bounds}) at zoom level: {zoom_level} with predicate: {predicate}"
    )
    tiles = list(mercantile.tiles(*geometry.bounds, zoom_level))
    quadkeys_boxes = [
        (mercantile.quadkey(t), box(*mercantile.bounds(t))) for t in tiles
    ]
    quadkeys, boxes = zip(*quadkeys_boxes) if quadkeys_boxes else ([], [])

    if not boxes:
        cls.logger.warning(
            "No boxes generated from geometry bounds. Returning empty MercatorTiles."
        )
        return MercatorTiles(zoom_level=zoom_level, quadkeys=[])

    s = STRtree(boxes)
    result_indices = s.query(geometry, predicate=predicate)
    filtered_quadkeys = [quadkeys[i] for i in result_indices]
    cls.logger.info(
        f"Filtered down to {len(filtered_quadkeys)} quadkeys using spatial predicate."
    )
    return cls(zoom_level=zoom_level, quadkeys=filtered_quadkeys, **kwargs)
from_json(data_store, file, **kwargs) classmethod

Load MercatorTiles from a JSON file.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_json(
    cls, data_store: DataStore, file: Union[str, Path], **kwargs
) -> "MercatorTiles":
    """Load MercatorTiles from a JSON file."""
    cls.logger.info(
        f"Loading MercatorTiles from JSON file: {file} using data store: {type(data_store).__name__}"
    )
    with data_store.open(str(file), "r") as f:
        data = json.load(f)
        if isinstance(data, list):  # If file contains only quadkeys
            data = {
                "zoom_level": len(data[0]) if data else 0,
                "quadkeys": data,
                **kwargs,
            }
        else:
            data.update(kwargs)
        instance = cls(**data)
        instance.data_store = data_store
        cls.logger.info(
            f"Successfully loaded {len(instance.quadkeys)} quadkeys from JSON file."
        )
        return instance
from_points(points, zoom_level, **kwargs) classmethod

Create MercatorTiles from a list of points or lat-lon pairs.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_points(
    cls, points: List[Union[Point, Tuple[float, float]]], zoom_level: int, **kwargs
) -> "MercatorTiles":
    """Create MercatorTiles from a list of points or lat-lon pairs."""
    cls.logger.info(
        f"Creating MercatorTiles from {len(points)} points at zoom level: {zoom_level}"
    )
    quadkeys = {
        (
            mercantile.quadkey(mercantile.tile(p.x, p.y, zoom_level))
            if isinstance(p, Point)
            else mercantile.quadkey(mercantile.tile(p[1], p[0], zoom_level))
        )
        for p in points
    }
    cls.logger.info(f"Generated {len(quadkeys)} unique quadkeys from points.")
    return cls(zoom_level=zoom_level, quadkeys=list(quadkeys), **kwargs)
from_quadkeys(quadkeys) classmethod

Create MercatorTiles from list of quadkeys.

Source code in gigaspatial/grid/mercator_tiles.py
@classmethod
def from_quadkeys(cls, quadkeys: List[str]):
    """Create MercatorTiles from list of quadkeys."""
    if not quadkeys:
        cls.logger.warning("No quadkeys provided to from_quadkeys.")
        return cls(zoom_level=0, quadkeys=[])
    return cls(zoom_level=len(quadkeys[0]), quadkeys=set(quadkeys))
save(file, format='json')

Save MercatorTiles to file in specified format.

Source code in gigaspatial/grid/mercator_tiles.py
def save(self, file: Union[str, Path], format: str = "json") -> None:
    """Save MercatorTiles to file in specified format."""
    with self.data_store.open(str(file), "wb" if format == "parquet" else "w") as f:
        if format == "parquet":
            self.to_geodataframe().to_parquet(f, index=False)
        elif format == "geojson":
            f.write(self.to_geodataframe().to_json(drop_id=True))
        elif format == "json":
            json.dump(self.quadkeys, f)
        else:
            raise ValueError(f"Unsupported format: {format}")
to_dataframe()

Convert to pandas DataFrame with quadkey and centroid coordinates.

Source code in gigaspatial/grid/mercator_tiles.py
def to_dataframe(self) -> pd.DataFrame:
    """Convert to pandas DataFrame with quadkey and centroid coordinates."""
    self.logger.info(
        f"Converting {len(self.quadkeys)} quadkeys to pandas DataFrame."
    )
    if not self.quadkeys:
        self.logger.warning(
            "No quadkeys to convert to DataFrame. Returning empty DataFrame."
        )
        return pd.DataFrame(columns=["quadkey", "latitude", "longitude"])
    tiles_data = [mercantile.quadkey_to_tile(q) for q in self.quadkeys]
    bounds_data = [mercantile.bounds(tile) for tile in tiles_data]

    centroids = [
        (
            (bounds.south + bounds.north) / 2,  # latitude
            (bounds.west + bounds.east) / 2,  # longitude
        )
        for bounds in bounds_data
    ]

    self.logger.info(f"Successfully converted to DataFrame.")

    return pd.DataFrame(
        {
            "quadkey": self.quadkeys,
            "latitude": [c[0] for c in centroids],
            "longitude": [c[1] for c in centroids],
        }
    )
to_geodataframe()

Convert to GeoPandas GeoDataFrame.

Source code in gigaspatial/grid/mercator_tiles.py
def to_geodataframe(self) -> gpd.GeoDataFrame:
    """Convert to GeoPandas GeoDataFrame."""
    return gpd.GeoDataFrame(
        {"quadkey": self.quadkeys, "geometry": self.to_geoms()}, crs="EPSG:4326"
    )