developmentseed / developmentseed/stac-react

Add Generic STAC Resource Fetching

Open
#35 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
34
Forks
4
PR merge metrics
No merged PRs in 30d

Description

## Problem
Currently, stac-react assumes you're working with a structured STAC API with known endpoints (`/collections`, `/search`, etc.). There's no way to fetch an arbitrary STAC resource (catalog, collection, or item) from any URL without knowing its type in advance.

This is problematic when:
- Following STAC links where the target type is unknown
- Exploring static STAC catalogs
- Working with STAC resources from various sources
- Building generic STAC browsers/explorers

## Current Behavior
You must know the resource type and use the specific hook:
```typescript
// Must know it's a collection
const { collection } = useCollection('collection-id');

// Must know it's an item
const { item } = useItem(itemUrl);

// No way to fetch "whatever is at this URL"
```

## Desired Behavior
```typescript
import { useStacValue } from '@developmentseed/stac-react';

function StacResourceViewer({ url }) {
const { value, type, isLoading, error } = useStacValue(url);

// value could be Catalog, Collection, Item, or ItemCollection
// type indicates what was fetched: 'Catalog' | 'Collection' | 'Feature' | 'FeatureCollection'

switch (type) {
case 'Catalog':
return ;
case 'Collection':
return ;
case 'Feature':
return ;
case 'FeatureCollection':
return ;
}
}
```

## Use Cases from stac-map

stac-map has a `useStacValue` hook that:
```typescript
const { value, error, items, table } = useStacValue({
href: 'https://any-stac-url.com/resource.json',
fileUpload,
datetimeBounds,
stacGeoparquetItemId,
});

// value is automatically typed based on what was fetched
// Works with URLs, file uploads, and geoparquet
```

## Proposed Solution

### Hook Signature
```typescript
type StacValueType = 'Catalog' | 'Collection' | 'Feature' | 'FeatureCollection';

type StacValue = StacCatalog | StacCollection | StacItem | StacItemCollection;

type UseStacValueResult = {
value?: StacValue;
type?: StacValueType;
isLoading: boolean;
isFetching: boolean;
error?: ApiErrorType;
refetch: () => Promise;
};

function useStacValue(
url: string,
options?: {
headers?: Record;
method?: 'GET' | 'POST';
body?: unknown;
}
): UseStacValueResult;
```

### Implementation
```typescript
import { useQuery } from '@tanstack/react-query';

function useStacValue(url: string, options = {}): UseStacValueResult {
const { data, error, isLoading, isFetching, refetch } = useQuery({
queryKey: ['stac-value', url, options],
queryFn: async () => {
const response = await fetch(url, {
method: options.method || 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
});

if (!response.ok) {
throw new ApiError(
response.statusText,
response.status,
await response.text(),
response.url
);
}

const json = await response.json();
return {
value: json,
type: determineStacType(json),
};
},
enabled: !!url,
retry: false,
});

return {
value: data?.value,
type: data?.type,
isLoading,
isFetching,
error,
refetch,
};
}

function determineStacType(json: any): StacValueType {
if (json.type === 'Feature') return 'Feature';
if (json.type === 'FeatureCollection') return 'FeatureCollection';
if (json.type === 'Collection') return 'Collection';
if (json.type === 'Catalog') return 'Catalog';

// Fallback: try to infer from structure
if (json.extent && json.license) return 'Collection';
if (json.links && !json.features) return 'Catalog';

throw new Error('Unable to determine STAC type from resource');
}
```

## Advanced Features

### Type Guards
```typescript
export function isStacCatalog(value: StacValue): value is StacCatalog {
return value.type === 'Catalog';
}

export function isStacCollection(value: StacValue): value is StacCollection {
return value.type === 'Collection';
}

export function isStacItem(value: StacValue): value is StacItem {
return value.type === 'Feature';
}

export function isStacItemCollection(value: StacValue): value is StacItemCollection {
return value.type === 'FeatureCollection';
}
```

### Link Resolution
Optionally resolve relative hrefs to absolute:
```typescript
function makeHrefsAbsolute(value: StacValue, baseUrl: string): StacValue {
if (!value.links) return value;

return {
...value,
links: value.links.map(link => ({
...link,
href: new URL(link.href, baseUrl).toString(),
})),
};
}
```

## Benefits
- ✅ Generic STAC resource fetching
- ✅ Type-safe handling of different STAC types
- ✅ Simplifies link following in STAC catalogs
- ✅ Enables building generic STAC explorers
- ✅ Matches patterns used in other STAC tools

## Breaking Changes
None - this is new functionality.

## Related Issues
- #34 - Add Standalone Hooks (Context-Free Mode)

## Testing Requirements
- [ ] Test with STAC catalogs
- [ ] Test with STAC collections
- [ ] Test with STAC items
- [ ] Test with item collections
- [ ] Test type determination logic
- [ ] Test error handling for invalid STAC
- [ ] Test with relative and absolute hrefs

## Documentation Requirements
- [ ] Document the `useStacValue` hook
- [ ] Provide examples for each STAC type
- [ ] Document type guards and utilities
- [ ] Show link following patterns

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.