Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ tilemaker = ["server/static/*", "server/static/assets/*"]

[project]
name = "tilemaker"
version = "2.6.0"
version = "3.0.0"
requires-python = ">=3.10"
dependencies = [
"pixell",
Expand Down
1 change: 1 addition & 0 deletions tilemaker/client/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def create_sample_metadata(filename: str):

return [
MapGroup(
map_group_id="example-map-group",
name="Map Group",
description="Example",
maps=[
Expand Down
70 changes: 67 additions & 3 deletions tilemaker/metadata/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pydantic import BaseModel

from .boxes import Box
from .definitions import Layer, MapGroup
from .definitions import Band, Layer, LayerWithMenuState, MapGroup, SearchResponse
from .sources import SourceGroup


Expand All @@ -22,13 +22,77 @@ def merge(self, other: "DataConfiguration") -> "DataConfiguration":
source_groups=self.source_groups + other.source_groups,
)

def _match(self, name: str, query: str) -> bool:
return query.lower() in name.lower()

def filter_map_groups(
self, authorized_map_groups: list, query: str
) -> SearchResponse:
matched_ids: set[str] = set()
filtered_groups = []

for group in authorized_map_groups:
# Group name matches — keep entire subtree intact
if self._match(group["name"], query):
matched_ids.add(group["map_group_id"])
filtered_groups.append(group)
continue

filtered_maps = []
for map in group.get("maps", []):
# Map name matches — keep entire subtree intact
if self._match(map["name"], query):
matched_ids.add(map["map_id"])
filtered_maps.append(map)
continue

filtered_bands = []
for band in map.get("bands", []):
# Band name matches — keep entire subtree intact
if self._match(band["name"], query):
matched_ids.add(band["band_id"])
filtered_bands.append(band)
continue

filtered_layers = [
layer
for layer in band.get("layers", [])
if self._match(layer["name"], query)
and matched_ids.add(layer["layer_id"]) is None
]
if filtered_layers:
filtered_bands.append({**band, "layers": filtered_layers})

if filtered_bands:
filtered_maps.append({**map, "bands": filtered_bands})

if filtered_maps:
filtered_groups.append({**group, "maps": filtered_maps})

return SearchResponse(
filtered_layer_menu=filtered_groups,
matched_ids=list(matched_ids),
)

@property
def layers(self) -> Iterable[Layer]:
def bands(self) -> Iterable[Band]:
return itertools.chain.from_iterable(
band.layers
map.bands for group in self.map_groups for map in group.maps
)

@property
def layers(self) -> Iterable[LayerWithMenuState]:
return (
LayerWithMenuState(
**layer.model_dump(),
map_group_id=group.map_group_id,
map_id=map.map_id,
band_id=band.band_id,
)
for group in self.map_groups
for map in group.maps
for band in map.bands
for layer in band.layers
)

def layer(self, layer_id: str) -> Layer | None:
Expand Down
124 changes: 121 additions & 3 deletions tilemaker/metadata/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
from .core import DataConfiguration
from .definitions import (
Band,
BandMenuState,
Layer,
LayerSummary,
LayerWithMenuState,
Map,
MapGroup,
MapGroupMenuState,
MapMenuState,
SearchResponse,
)
from .fits import FITSCombinationLayerProvider, FITSLayerProvider
from .orm import (
Expand Down Expand Up @@ -61,6 +67,103 @@ def create_tables(self):
"""Create all tables in the database."""
Base.metadata.create_all(self.engine)

def filter_map_groups(self, _: list, query: str) -> dict:
"""
Use database queries to filter map groups based on user's query string
"""
from sqlalchemy import or_

pattern = f"%{query}%"

with self.session_maker() as session:
hits = (
session.query(LayerORM, BandORM, MapORM, MapGroupORM)
.join(BandORM, LayerORM.band_id == BandORM.id)
.join(MapORM, BandORM.map_id == MapORM.id)
.join(MapGroupORM, MapORM.map_group_id == MapGroupORM.id)
.filter(
or_(
MapGroupORM.name.ilike(pattern),
MapORM.name.ilike(pattern),
BandORM.name.ilike(pattern),
LayerORM.name.ilike(pattern),
)
)
.all()
)

matched_ids: set[str] = set()

# Build the menu-state hierarchy purely from hit rows
# using ordered dicts to deduplicate by ID
group_map: dict[str, MapGroupMenuState] = {}
map_map: dict[str, MapMenuState] = {}
band_map: dict[tuple, BandMenuState] = {}

for layer_orm, band_orm, map_orm, group_orm in hits:
# Track what level matched to determine matched_ids
if query.lower() in group_orm.name.lower():
matched_ids.add(group_orm.map_group_id)
elif query.lower() in map_orm.name.lower():
matched_ids.add(map_orm.map_id)
elif query.lower() in band_orm.name.lower():
matched_ids.add(band_orm.band_id)
else:
matched_ids.add(layer_orm.layer_id)

layer_summary = LayerSummary(
layer_id=layer_orm.layer_id,
name=layer_orm.name,
description=layer_orm.description,
grant=layer_orm.grant,
)

band_key = (map_orm.map_id, band_orm.band_id)
if band_key not in band_map:
band_map[band_key] = BandMenuState(
band_id=band_orm.band_id,
name=band_orm.name,
description=band_orm.description or "",
grant=band_orm.grant,
layers=[],
)

if map_orm.map_id not in map_map:
map_map[map_orm.map_id] = MapMenuState(
map_id=map_orm.map_id,
name=map_orm.name,
description=map_orm.description or "",
grant=map_orm.grant,
bands=[],
)

if group_orm.map_group_id not in group_map:
group_map[group_orm.map_group_id] = MapGroupMenuState(
map_group_id=group_orm.map_group_id,
name=group_orm.name,
description=group_orm.description or "",
grant=group_orm.grant,
maps=[],
)

# Wire up the hierarchy
band_state = band_map[band_key]
if layer_summary not in band_state.layers:
band_state.layers.append(layer_summary)

map_state = map_map[map_orm.map_id]
if band_state not in map_state.bands:
map_state.bands.append(band_state)

group_state = group_map[group_orm.map_group_id]
if map_state not in group_state.maps:
group_state.maps.append(map_state)

return SearchResponse(
filtered_layer_menu=list(group_map.values()),
matched_ids=list(matched_ids),
)

@property
def map_groups(self) -> list[MapGroup]:
"""Retrieve all map groups from the database."""
Expand Down Expand Up @@ -88,13 +191,26 @@ def source_groups(self) -> list[SourceGroup]:
]

@property
def layers(self) -> Iterable[Layer]:
"""Retrieve all layers from the database."""
def bands(self) -> Iterable[Band]:
"""Retrieve all bands from the database."""
return itertools.chain.from_iterable(
band.layers
map.bands for group in self.map_groups for map in group.maps
)

@property
def layers(self) -> Iterable[LayerWithMenuState]:
"""Retrieve all layers from the database."""
return (
LayerWithMenuState(
**layer.model_dump(),
map_group_id=group.map_group_id,
map_id=map.map_id,
band_id=band.band_id,
)
for group in self.map_groups
for map in group.maps
for band in map.bands
for layer in band.layers
)

def layer(self, layer_id: str) -> Layer | None:
Expand Down Expand Up @@ -262,6 +378,7 @@ def _orm_to_map_group(self, session: Session, orm_group: MapGroupORM) -> MapGrou
description=orm_group.description,
maps=maps,
grant=orm_group.grant,
map_group_id=orm_group.map_group_id,
)

def populate_from_config(self, config: "DataConfiguration") -> None:
Expand All @@ -286,6 +403,7 @@ def populate_from_config(self, config: "DataConfiguration") -> None:
name=map_group.name,
description=map_group.description,
grant=map_group.grant,
map_group_id=map_group.map_group_id,
)
session.add(orm_group)
session.flush()
Expand Down
45 changes: 41 additions & 4 deletions tilemaker/metadata/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ def auth(self, grants: set[str]):
return self.grant is None or self.grant in grants


class Layer(AuthenticatedModel):
class LayerSummary(AuthenticatedModel):
layer_id: str
name: str
description: str | None = None


class Layer(LayerSummary):
provider: FITSLayerProvider | FITSCombinationLayerProvider

bounding_left: float | None = None
Expand Down Expand Up @@ -115,26 +117,47 @@ def model_post_init(self, _):
self.tile_size, self.number_of_levels = self.provider.calculate_tile_size()


class Band(AuthenticatedModel):
class LayerWithMenuState(Layer):
map_group_id: str
map_id: str
band_id: str


class BandBase(AuthenticatedModel):
band_id: str
name: str
description: str


class Band(BandBase):
layers: list[Layer]


class Map(AuthenticatedModel):
class BandMenuState(BandBase):
layers: list[LayerSummary]


class MapBase(AuthenticatedModel):
map_id: str
name: str
description: str


class Map(MapBase):
bands: list[Band]


class MapGroup(AuthenticatedModel):
class MapMenuState(MapBase):
bands: list[BandMenuState]


class MapGroupBase(AuthenticatedModel):
map_group_id: str
name: str
description: str


class MapGroup(MapGroupBase):
maps: list[Map]

def get_layer(self, layer_id: str) -> Layer | None:
Expand All @@ -145,3 +168,17 @@ def get_layer(self, layer_id: str) -> Layer | None:
return layer

return None


class MapGroupMenuState(MapGroupBase):
maps: list[MapMenuState]


class LayerDefault(AuthenticatedModel):
layer: LayerWithMenuState | None
default_layer_menu: list[MapGroupMenuState]


class SearchResponse(AuthenticatedModel):
filtered_layer_menu: list[MapGroupMenuState]
matched_ids: list[str]
6 changes: 5 additions & 1 deletion tilemaker/metadata/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
import uuid
from hashlib import md5
from pathlib import Path
from typing import Any, Literal
Expand Down Expand Up @@ -76,7 +77,10 @@ def map_group_from_fits(
)

return MapGroup(
name="Auto-Populated", description="No description provided", maps=maps
map_group_id=f"map-group-{uuid.uuid4()}",
name="Auto-Populated",
description="No description provided",
maps=maps,
)


Expand Down
1 change: 1 addition & 0 deletions tilemaker/metadata/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class MapGroupORM(Base):
__tablename__ = "map_groups"

id = Column(Integer, primary_key=True)
map_group_id = Column(String, unique=True, nullable=False)
name = Column(String, nullable=False)
description = Column(String)
grant = Column(String)
Expand Down
Loading
Loading