Skip to content
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_map_groups=filtered_groups,
matched_ids=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_map_groups=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