Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions src/context/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createContext } from 'react';
import StacApi from '../stac-api';
import { CollectionsResponse, Item } from '../types/stac';

type StacApiContextType = {
stacApi?: StacApi;
collections?: CollectionsResponse;
setCollections: (collections?: CollectionsResponse) => void;
getItem: (id: string) => Item | undefined;
addItem: (id: string, item: Item) => void;
deleteItem: (id: string) => void;
};

export const StacApiContext = createContext<StacApiContextType>(
{} as StacApiContextType
);
79 changes: 37 additions & 42 deletions src/context/index.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,65 @@
import React, { useMemo, useContext, useState, useCallback } from 'react';
import { createContext } from 'react';

import StacApi from '../stac-api';
import useStacApi from '../hooks/useStacApi';
import type { CollectionsResponse, Item } from '../types/stac';
import { GenericObject } from '../types';

type StacApiContextType = {
stacApi?: StacApi;
collections?: CollectionsResponse;
setCollections: (collections?: CollectionsResponse) => void;
getItem: (id: string) => Item | undefined;
addItem: (id: string, item: Item) => void;
deleteItem: (id: string) => void;
}
import { StacApiContext } from './context';

type StacApiProviderType = {
apiUrl: string;
children: React.ReactNode;
options?: GenericObject;
}
};

export const StacApiContext = createContext<StacApiContextType>({} as StacApiContextType);

export function StacApiProvider({ children, apiUrl, options }: StacApiProviderType) {
export function StacApiProvider({
children,
apiUrl,
options
}: StacApiProviderType) {
const { stacApi } = useStacApi(apiUrl, options);
const [ collections, setCollections ] = useState<CollectionsResponse>();
const [ items, setItems ] = useState(new Map<string, Item>());
const [collections, setCollections] = useState<CollectionsResponse>();
const [items, setItems] = useState(new Map<string, Item>());

const getItem = useCallback((id: string) => items.get(id), [items]);

const addItem = useCallback((itemPath: string, item: Item) => {
setItems(new Map(items.set(itemPath, item)));
}, [items]);
const addItem = useCallback(
(itemPath: string, item: Item) => {
setItems(new Map(items.set(itemPath, item)));
},
[items]
);

const deleteItem = useCallback((itemPath: string) => {
const tempItems = new Map(items);
items.delete(itemPath);
setItems(tempItems);
}, [items]);
const deleteItem = useCallback(
(itemPath: string) => {
const tempItems = new Map(items);
items.delete(itemPath);
setItems(tempItems);
},
[items]
);

const contextValue = useMemo(() => ({
stacApi,
collections,
setCollections,
getItem,
addItem,
deleteItem
}), [addItem, collections, deleteItem, getItem, stacApi]);
const contextValue = useMemo(
() => ({
stacApi,
collections,
setCollections,
getItem,
addItem,
deleteItem
}),
[addItem, collections, deleteItem, getItem, stacApi]
);

return (
<StacApiContext.Provider value={contextValue}>
{ children }
{children}
</StacApiContext.Provider>
);
}

export function useStacApiContext() {
const {
stacApi,
collections,
setCollections,
getItem,
addItem,
deleteItem
} = useContext(StacApiContext);
const { stacApi, collections, setCollections, getItem, addItem, deleteItem } =
useContext(StacApiContext);

return {
stacApi,
Expand Down
74 changes: 50 additions & 24 deletions src/hooks/useCollection.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,70 @@
import { useMemo, useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';

import type { ApiError, LoadingState } from '../types';
import type { Collection } from '../types/stac';
import useCollections from './useCollections';
import { useStacApiContext } from '../context';

type StacCollectionHook = {
collection?: Collection,
state: LoadingState,
error?: ApiError,
reload: () => void
collection?: Collection;
state: LoadingState;
error?: ApiError;
reload: () => void;
};

function useCollection(collectionId: string): StacCollectionHook {
const { collections, state, error: requestError, reload } = useCollections();
const [ error, setError ] = useState<ApiError>();
if (!collectionId) {
throw new Error('Collection ID is required');
}

useEffect(() => {
setError(requestError);
}, [requestError]);

const collection = useMemo(
() => {
const coll = collections?.collections.find(({ id }) => id === collectionId);
if (!coll) {
setError({
status: 404,
statusText: 'Not found',
detail: 'Collection does not exist'
});
const { stacApi, collections } = useStacApiContext();

const [collection, setCollection] = useState<Collection>();
const [state, setState] = useState<LoadingState>('IDLE');
const [error, setError] = useState<ApiError>();

const load = useCallback(
(id: string) => {
if (stacApi) {
setError(undefined);
setState('LOADING');
stacApi
.getCollection(id)
.then(async (res) => {
const data: Collection = await res.json();
setCollection(data);
})
.catch((err: ApiError) => {
setError(err);
})
.finally(() => {
setState('IDLE');
});
}
return coll;
},
[collectionId, collections]
[stacApi]
);

useEffect(() => {
setState('LOADING');
// Check if the collection is already in the collections list.
const coll = collections?.collections.find(({ id }) => id === collectionId);
if (coll) {
setCollection(coll);
setState('IDLE');
return;
}

// If not, request the collection directly from the API.
load(collectionId);
}, [collectionId, collections, load]);

return {
collection,
state,
error,
reload
reload: useCallback(() => {
load(collectionId);
}, [collectionId, load])
};
}

Expand Down
93 changes: 64 additions & 29 deletions src/hooks/useCollections.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,90 @@
import { useCallback, useEffect, useState, useMemo } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { type ApiError, type LoadingState } from '../types';
import type { CollectionsResponse } from '../types/stac';
import debounce from '../utils/debounce';
import { useStacApiContext } from '../context';

type StacCollectionsHook = {
collections?: CollectionsResponse,
reload: () => void,
state: LoadingState
error?: ApiError
collections?: CollectionsResponse;
reload: () => void;
state: LoadingState;
error?: ApiError;
nextPage?: () => void;
prevPage?: () => void;
setOffset: (newOffset: number) => void;
};

function useCollections(): StacCollectionsHook {
export default function useCollections(opts?: {
limit?: number;
initialOffset?: number;
}): StacCollectionsHook {
const { limit = 10, initialOffset = 0 } = opts || {};

const { stacApi, collections, setCollections } = useStacApiContext();
const [ state, setState ] = useState<LoadingState>('IDLE');
const [ error, setError ] = useState<ApiError>();
const [state, setState] = useState<LoadingState>('IDLE');
const [error, setError] = useState<ApiError>();

const [offset, setOffset] = useState(initialOffset);

const [hasNext, setHasNext] = useState(false);
const [hasPrev, setHasPrev] = useState(false);

const _getCollections = useCallback(
() => {
async (offset: number, limit: number) => {
if (stacApi) {
setState('LOADING');

stacApi.getCollections()
.then(response => response.json())
.then(setCollections)
.catch((err) => {
setError(err);
setCollections(undefined);
})
.finally(() => setState('IDLE'));
try {
const res = await stacApi.getCollections({ limit, offset });
const data: CollectionsResponse = await res.json();

setHasNext(!!data.links?.find((l) => l.rel === 'next'));
setHasPrev(
!!data.links?.find((l) => ['prev', 'previous'].includes(l.rel))
);

setCollections(data);
} catch (err: any) {
setError(err);
setCollections(undefined);
} finally {
setState('IDLE');
}
}
},
[setCollections, stacApi]
);
const getCollections = useMemo(() => debounce(_getCollections), [_getCollections]);

useEffect(
() => {
if (stacApi && !error && !collections) {
getCollections();
}
},
[getCollections, stacApi, collections, error]
const getCollections = useCallback(
(offset: number, limit: number) =>
debounce(() => _getCollections(offset, limit))(),
[_getCollections]
);

const nextPage = useCallback(() => {
setOffset(offset + limit);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the collections response has next and/or previous links specified, why are we using offset and limit here instead of calling the provided links. Also, the collections spec uses page query params for pagination in collections.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While working with the eoepca api (https://eoapi.apx.develop.eoepca.org/stac/collections?limit=2) it uses offset and limit, so I went with that.
Using the offset and limit allows the user to go to a specific page, or to reset to the first page more easily. There's no "first" link, so to go back we'd need to get the current link and set the offset to 0.

Not sure what the best approach would be. Any ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I said was not 100% correct. The spec actually says this:

With this, a link with relation next is included in the links array, and this is used to navigate to the next page of Collection objects. The specific query parameter used for paging is implementation specific and not defined by STAC API. For example, an implementation may take a parameter (e.g., page) indicating the numeric page of results, a base64-encoded value indicating the last result returned for the current page (e.g., search_after as in Elasticsearch), or a cursor token representing backend state.

So really, we don't know how individual services implement pagination. Seeing, that STAC React is a generic library, the safest bet is to rely on next and prev/previous links.

Not being able to reliably infer a first or last page link is an obvious flaw, which should probably be added to the STAC API spec. We could try to infer the pagination parameters, but it seems to open a huge can of worms because we really don't know how pagination will work in each instance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is unfortunate. I'll check about using the prev/next

}, [offset, limit]);

const prevPage = useCallback(() => {
setOffset(offset - limit);
}, [offset, limit]);

useEffect(() => {
if (stacApi && !error && !collections) {
getCollections(offset, limit);
}
}, [getCollections, stacApi, collections, error, offset, limit]);

return {
collections,
reload: getCollections,
reload: useCallback(
() => getCollections(offset, limit),
[getCollections, offset, limit]
),
nextPage: hasNext ? nextPage : undefined,
prevPage: hasPrev ? prevPage : undefined,
setOffset,
state,
error,
error
};
}

export default useCollections;
Loading