from __future__ import annotations
import json
from collections.abc import Generator
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from py3dtiles.typing import AssetDictType, GeometricErrorType, TilesetDictType
from .root_property import RootProperty
from .tile import Tile
if TYPE_CHECKING:
from typing_extensions import Self
from .content import TileContent
[docs]
class Asset(RootProperty[AssetDictType]):
def __init__(
self, version: Literal["1.0", "1.1"] = "1.0", tileset_version: str | None = None
) -> None:
super().__init__()
self.version = version
self.tileset_version = tileset_version
[docs]
@classmethod
def from_dict(cls, asset_dict: AssetDictType) -> Self:
asset = cls(asset_dict["version"])
if "tilesetVersion" in asset_dict:
asset.tileset_version = asset_dict["tilesetVersion"]
asset.set_properties_from_dict(asset_dict)
return asset
[docs]
def to_dict(self) -> AssetDictType:
asset_dict: AssetDictType = {"version": self.version}
asset_dict = self.add_root_properties_to_dict(asset_dict)
if self.tileset_version is not None:
asset_dict["tilesetVersion"] = self.tileset_version
return asset_dict
[docs]
class TileSet(RootProperty[TilesetDictType]):
"""
This class represents the tileset in a 3DTiles dataset.
"""
def __init__(
self,
geometric_error: float = 500,
) -> None:
super().__init__()
self.asset = Asset(version="1.0")
self.geometric_error: GeometricErrorType = geometric_error
self.root_tile = Tile()
self.extensions_used: set[str] = set()
self.extensions_required: set[str] = set()
# root_uri is used when we build a tileset in-memory from an existing tileset and want to download tiles later for instance
self.root_uri: Path | None = None
[docs]
@classmethod
def from_dict(cls, tileset_dict: TilesetDictType) -> Self:
tileset = cls(geometric_error=tileset_dict["geometricError"])
tileset.asset = Asset.from_dict(tileset_dict["asset"])
tileset.root_tile = Tile.from_dict(tileset_dict["root"])
tileset.set_properties_from_dict(tileset_dict)
if "extensionsUsed" in tileset_dict:
tileset.extensions_used = set(tileset_dict["extensionsUsed"])
if "extensionsRequired" in tileset_dict:
tileset.extensions_required = set(tileset_dict["extensionsRequired"])
return tileset
[docs]
@staticmethod
def from_file(tileset_path: Path) -> TileSet:
with tileset_path.open() as f:
tileset_dict = json.load(f)
tileset = TileSet.from_dict(tileset_dict)
tileset.root_uri = tileset_path.parent
return tileset
[docs]
def get_all_tile_contents(
self,
) -> Generator[TileContent | TileSet, None, None]:
tiles = [self.root_tile] + self.root_tile.get_all_children()
for tile in tiles:
yield tile.get_or_fetch_content(self.root_uri)
[docs]
def delete_on_disk(
self, tileset_path: Path, delete_sub_tileset: bool = False
) -> None:
"""Deletes all files linked to the tileset. The uri of the tileset should be defined.
:param tileset_path: The path of the tileset
:param delete_sub_tileset: If True, all tilesets present as tile content will be removed as well as their content. If False, the linked tilesets in tiles won't be removed.
"""
tileset_path.unlink()
self.root_tile.delete_on_disk(tileset_path.parent, delete_sub_tileset)
[docs]
def write_to_directory(self, tileset_path: Path, overwrite: bool = False) -> None:
"""
Write (or overwrite), to the directory whose name is provided, the
TileSet that is:
- the tileset as a json file and
- all the tiles content of the Tiles used by the Tileset.
:param tileset_path: the target directory name
:param overwrite: delete the tileset (and the content) if already exists
"""
if tileset_path.exists():
if overwrite:
tileset = TileSet.from_file(tileset_path)
tileset.delete_on_disk(tileset_path, delete_sub_tileset=True)
else:
raise FileExistsError(
f"There is already a file at {tileset_path} and overwrite is False"
)
# Proceed with the writing of the TileSet per se:
self.write_as_json(tileset_path)
# Prior to writing the TileSet, the future location of the enclosed
# Tile's content (set as their respective TileContent uri) must be
# specified:
all_tiles = self.root_tile.get_all_children()
all_tiles.append(self.root_tile)
for tile in all_tiles:
if tile.tile_content is not None:
tile.write_content(tileset_path.parent)
[docs]
def write_as_json(self, tileset_path: Path) -> None:
"""
Write the tileset as a JSON file.
:param tileset_path: the path where the tileset will be written
"""
with tileset_path.open("w") as f:
f.write(self.to_json())
[docs]
def to_dict(self) -> TilesetDictType:
"""
Convert to json string possibly mentioning used schemas
"""
# Make sure the TileSet is aligned with its children Tiles.
self.root_tile.sync_bounding_volume_with_children()
tileset_dict: TilesetDictType = {
"root": self.root_tile.to_dict(),
"asset": self.asset.to_dict(),
"geometricError": self.geometric_error,
}
tileset_dict = self.add_root_properties_to_dict(tileset_dict)
if self.extensions_used:
tileset_dict["extensionsUsed"] = list(self.extensions_used)
if self.extensions_required:
tileset_dict["extensionsRequired"] = list(self.extensions_required)
return tileset_dict
[docs]
def to_json(self) -> str:
return json.dumps(self.to_dict(), separators=(",", ":"))