|
3 | 3 | import re
|
4 | 4 | from typing import Dict, List, Literal, Optional, Tuple, get_args
|
5 | 5 |
|
| 6 | +from ciso8601 import parse_rfc3339 |
6 | 7 | from morecantile import Tile
|
7 | 8 | from morecantile import tms as default_tms
|
8 | 9 | from pygeofilter.ast import AstType
|
9 | 10 | from pygeofilter.parsers.cql2_json import parse as cql2_json_parser
|
10 | 11 | from pygeofilter.parsers.cql2_text import parse as cql2_text_parser
|
11 | 12 | from typing_extensions import Annotated
|
12 | 13 |
|
13 |
| -from tipg.collections import Catalog, Collection |
| 14 | +from tipg.collections import Catalog, Collection, CollectionList |
14 | 15 | from tipg.errors import InvalidBBox, MissingCollectionCatalog, MissingFunctionParameter
|
15 | 16 | from tipg.resources.enums import MediaType
|
16 | 17 | from tipg.settings import TMSSettings
|
17 | 18 |
|
18 |
| -from fastapi import HTTPException, Path, Query |
| 19 | +from fastapi import Depends, HTTPException, Path, Query |
19 | 20 |
|
20 | 21 | from starlette.requests import Request
|
21 | 22 |
|
|
30 | 31 | FilterLang = Literal["cql2-text", "cql2-json"]
|
31 | 32 |
|
32 | 33 |
|
33 |
| -def CollectionParams( |
34 |
| - request: Request, |
35 |
| - collectionId: Annotated[str, Path(description="Collection identifier")], |
36 |
| -) -> Collection: |
37 |
| - """Return Layer Object.""" |
38 |
| - collection_pattern = re.match( # type: ignore |
39 |
| - r"^(?P<schema>.+)\.(?P<collection>.+)$", collectionId |
| 34 | +def s_intersects(bbox: List[float], spatial_extent: List[float]) -> bool: |
| 35 | + """Check if bbox intersects with spatial extent.""" |
| 36 | + return ( |
| 37 | + (bbox[0] < spatial_extent[2]) |
| 38 | + and (bbox[2] > spatial_extent[0]) |
| 39 | + and (bbox[3] > spatial_extent[1]) |
| 40 | + and (bbox[1] < spatial_extent[3]) |
40 | 41 | )
|
41 |
| - if not collection_pattern: |
42 |
| - raise HTTPException( |
43 |
| - status_code=422, detail=f"Invalid Collection format '{collectionId}'." |
44 |
| - ) |
45 | 42 |
|
46 |
| - assert collection_pattern.groupdict()["schema"] |
47 |
| - assert collection_pattern.groupdict()["collection"] |
48 | 43 |
|
49 |
| - collection_catalog: Catalog = getattr(request.app.state, "collection_catalog", None) |
50 |
| - if not collection_catalog: |
51 |
| - raise MissingCollectionCatalog("Could not find collections catalog.") |
| 44 | +def t_intersects(interval: List[str], temporal_extent: List[str]) -> bool: |
| 45 | + """Check if dates intersect with temporal extent.""" |
| 46 | + if len(interval) == 1: |
| 47 | + start = end = parse_rfc3339(interval[0]) |
52 | 48 |
|
53 |
| - if collectionId in collection_catalog["collections"]: |
54 |
| - return collection_catalog["collections"][collectionId] |
| 49 | + else: |
| 50 | + start = parse_rfc3339(interval[0]) if interval[0] not in ["..", ""] else None |
| 51 | + end = parse_rfc3339(interval[1]) if interval[1] not in ["..", ""] else None |
55 | 52 |
|
56 |
| - raise HTTPException( |
57 |
| - status_code=404, detail=f"Table/Function '{collectionId}' not found." |
58 |
| - ) |
| 53 | + mint, maxt = temporal_extent |
| 54 | + min_ext = parse_rfc3339(mint) if mint is not None else None |
| 55 | + max_ext = parse_rfc3339(maxt) if maxt is not None else None |
59 | 56 |
|
| 57 | + if len(interval) == 1: |
| 58 | + if start == min_ext or start == max_ext: |
| 59 | + return True |
60 | 60 |
|
61 |
| -def CatalogParams(request: Request) -> Catalog: |
62 |
| - """Return Collections Catalog.""" |
63 |
| - collection_catalog: Catalog = getattr(request.app.state, "collection_catalog", None) |
64 |
| - if not collection_catalog: |
65 |
| - raise MissingCollectionCatalog("Could not find collections catalog.") |
| 61 | + if not start: |
| 62 | + return max_ext <= end or min_ext <= end |
| 63 | + |
| 64 | + elif not end: |
| 65 | + return min_ext >= start or max_ext >= start |
66 | 66 |
|
67 |
| - return collection_catalog |
| 67 | + else: |
| 68 | + return min_ext >= start and max_ext <= end |
| 69 | + |
| 70 | + return False |
68 | 71 |
|
69 | 72 |
|
70 | 73 | def accept_media_type(accept: str, mediatypes: List[MediaType]) -> Optional[MediaType]:
|
@@ -397,3 +400,108 @@ def function_parameters_query( # noqa: C901
|
397 | 400 | )
|
398 | 401 |
|
399 | 402 | return function_parameters
|
| 403 | + |
| 404 | + |
| 405 | +def CollectionParams( |
| 406 | + request: Request, |
| 407 | + collectionId: Annotated[str, Path(description="Collection identifier")], |
| 408 | +) -> Collection: |
| 409 | + """Return Layer Object.""" |
| 410 | + collection_pattern = re.match( # type: ignore |
| 411 | + r"^(?P<schema>.+)\.(?P<collection>.+)$", collectionId |
| 412 | + ) |
| 413 | + if not collection_pattern: |
| 414 | + raise HTTPException( |
| 415 | + status_code=422, detail=f"Invalid Collection format '{collectionId}'." |
| 416 | + ) |
| 417 | + |
| 418 | + assert collection_pattern.groupdict()["schema"] |
| 419 | + assert collection_pattern.groupdict()["collection"] |
| 420 | + |
| 421 | + catalog: Catalog = getattr(request.app.state, "collection_catalog", None) |
| 422 | + if not catalog: |
| 423 | + raise MissingCollectionCatalog("Could not find collections catalog.") |
| 424 | + |
| 425 | + if collectionId in catalog["collections"]: |
| 426 | + return catalog["collections"][collectionId] |
| 427 | + |
| 428 | + raise HTTPException( |
| 429 | + status_code=404, detail=f"Table/Function '{collectionId}' not found." |
| 430 | + ) |
| 431 | + |
| 432 | + |
| 433 | +def CollectionsParams( |
| 434 | + request: Request, |
| 435 | + bbox_filter: Annotated[Optional[List[float]], Depends(bbox_query)], |
| 436 | + datetime_filter: Annotated[Optional[List[str]], Depends(datetime_query)], |
| 437 | + type_filter: Annotated[ |
| 438 | + Optional[Literal["Function", "Table"]], |
| 439 | + Query(alias="type", description="Filter based on Collection type."), |
| 440 | + ] = None, |
| 441 | + limit: Annotated[ |
| 442 | + Optional[int], |
| 443 | + Query( |
| 444 | + ge=0, |
| 445 | + le=1000, |
| 446 | + description="Limits the number of collection in the response.", |
| 447 | + ), |
| 448 | + ] = None, |
| 449 | + offset: Annotated[ |
| 450 | + Optional[int], |
| 451 | + Query( |
| 452 | + ge=0, |
| 453 | + description="Starts the response at an offset.", |
| 454 | + ), |
| 455 | + ] = None, |
| 456 | +) -> CollectionList: |
| 457 | + """Return Collections Catalog.""" |
| 458 | + limit = limit or 0 |
| 459 | + offset = offset or 0 |
| 460 | + |
| 461 | + catalog: Catalog = getattr(request.app.state, "collection_catalog", None) |
| 462 | + if not catalog: |
| 463 | + raise MissingCollectionCatalog("Could not find collections catalog.") |
| 464 | + |
| 465 | + collections_list = list(catalog["collections"].values()) |
| 466 | + |
| 467 | + # type filter |
| 468 | + if type_filter is not None: |
| 469 | + collections_list = [ |
| 470 | + collection |
| 471 | + for collection in collections_list |
| 472 | + if collection.type == type_filter |
| 473 | + ] |
| 474 | + |
| 475 | + # bbox filter |
| 476 | + if bbox_filter is not None: |
| 477 | + collections_list = [ |
| 478 | + collection |
| 479 | + for collection in collections_list |
| 480 | + if collection.bounds is not None |
| 481 | + and s_intersects(bbox_filter, collection.bounds) |
| 482 | + ] |
| 483 | + |
| 484 | + # datetime filter |
| 485 | + if datetime_filter is not None: |
| 486 | + collections_list = [ |
| 487 | + collection |
| 488 | + for collection in collections_list |
| 489 | + if collection.dt_bounds is not None |
| 490 | + and t_intersects(datetime_filter, collection.dt_bounds) |
| 491 | + ] |
| 492 | + |
| 493 | + matched = len(collections_list) |
| 494 | + |
| 495 | + if limit: |
| 496 | + collections_list = collections_list[offset : offset + limit] |
| 497 | + else: |
| 498 | + collections_list = collections_list[offset:] |
| 499 | + |
| 500 | + returned = len(collections_list) |
| 501 | + |
| 502 | + return CollectionList( |
| 503 | + collections=collections_list, |
| 504 | + matched=matched, |
| 505 | + next=offset + returned if matched - returned > offset else None, |
| 506 | + prev=max(offset - returned, 0) if offset else None, |
| 507 | + ) |
0 commit comments