Skip to content

Commit 89fd8fb

Browse files
authored
GEODES STAC API Search (#753)
* Add a STAC browser * Clean up * Improve _buildLayer for StacLayers * Swap start/end times to match geodes * Lint * Improve STAC hook * Save times in local storage * Use loading state on results panel * Prop clean * Update calendar and use UTC time * Adjust zoom amount when creating STAC layer * Css * Adjust result list CSS * Add rough draft metadata viewer * Add dropdown menu * Rework data objects * Move STAC browser to existing panel * Rework data objects * Remove panel from index * Improve type safety * I said I'd get around to it * Use filters object * Progress * Platform progress * Again * Add data / product dropdown section * Dropdown CSS * Fix icon css * Add badge * CSS * Add badges to show selections * Clean up some props * Don't close menu on select * Update hook query builder * Make it toggle * Clean up * Use Sets instead of arrays * Some CSS and cleaning up * Use discriminated union * Yes every commit is cleaning up * ugh * Remove metadata viewer * Don't query on first render * Remove log * Use StateDb * Adjust datepicker CSS * Remove model from dependency array * What does pre commit even do? * Change proj4 version * types * Add test * Use name from STAC item * Fix STAC layer visibility bug * Fix zoom to layer for STAC layers * Fix error spam in console when selecting STAC layer * Update fetchWithProxies * Update lite detection logic * Fix and debouce bbox update signal * Rename signal * Add checkbox to toggle whole world bbox * Update test for new proxy fetch * Really though * Fix proxy startegy logic * Update pagination number labels * Re-fetch query on bbox change * Remove old file * Fix duplicate import * Remove layer metadata stuff * Prepend all shared component css classnames * No model No fetch * Don't move map when adding result * Playwright appeasement
1 parent 5a93217 commit 89fd8fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3282
-346
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,6 @@ packages/schema/src/schema/geojson.json
145145
# Automatically generated file for processing
146146

147147
packages/schema/src/processing/_generated/*
148+
149+
# Local example files
150+
examples/local_examples/*

packages/base/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
"@lumino/widgets": "^2.0.0",
6565
"@mapbox/vector-tile": "^2.0.3",
6666
"@naisutech/react-tree": "^3.0.1",
67+
"@radix-ui/react-checkbox": "^1.3.2",
68+
"@radix-ui/react-dropdown-menu": "^2.1.15",
6769
"@radix-ui/react-popover": "^1.1.14",
6870
"@radix-ui/react-slot": "^1.2.3",
6971
"@radix-ui/react-tabs": "^1.1.12",
@@ -88,7 +90,7 @@
8890
"proj4": "^2.19.3",
8991
"proj4-list": "^1.0.4",
9092
"react": "^18.0.1",
91-
"react-day-picker": "8.10.1",
93+
"react-day-picker": "^9.7.0",
9294
"shpjs": "^6.1.0",
9395
"styled-components": "^5.3.6",
9496
"three": "^0.135.0",

packages/base/src/formbuilder/objectform/layer/layerform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class LayerPropertiesForm extends BaseForm {
4242
): void {
4343
super.processSchema(data, schema, uiSchema);
4444

45-
if (!schema.properties.source) {
45+
if (!schema.properties?.source) {
4646
return;
4747
}
4848

packages/base/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1+
export * from './annotations';
12
export * from './classificationModes';
23
export * from './commands/index';
34
export * from './constants';
45
export * from './dialogs/layerCreationFormDialog';
56
export * from './formbuilder/objectform/baseform';
67
export * from './icons';
78
export * from './mainview';
9+
export * from './menus';
810
export * from './panelview';
11+
export * from './stacBrowser';
912
export * from './store';
1013
export * from './toolbar';
1114
export * from './tools';
1215
export * from './types';
1316
export * from './widget';
14-
export * from './annotations';
15-
export * from './menus';

packages/base/src/mainview/mainView.tsx

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
IRasterLayer,
2323
IRasterSource,
2424
IShapefileSource,
25+
IStacLayer,
2526
IVectorLayer,
2627
IVectorTileLayer,
2728
IVectorTileSource,
@@ -51,15 +52,15 @@ import {
5152
VectorTile as VectorTileLayer,
5253
WebGLTile as WebGlTileLayer,
5354
} from 'ol/layer';
55+
import LayerGroup from 'ol/layer/Group';
5456
import TileLayer from 'ol/layer/Tile';
5557
import {
5658
fromLonLat,
57-
get as getRegisteredProjection,
59+
get as getProjection,
5860
toLonLat,
5961
transformExtent,
6062
} from 'ol/proj';
6163
import { register } from 'ol/proj/proj4.js';
62-
import { get as getProjection } from 'ol/proj.js';
6364
import RenderFeature from 'ol/render/Feature';
6465
import {
6566
GeoTIFF as GeoTIFFSource,
@@ -74,6 +75,7 @@ import { Circle, Fill, Stroke, Style } from 'ol/style';
7475
import { Rule } from 'ol/style/flat';
7576
//@ts-expect-error no types for ol-pmtiles
7677
import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
78+
import StacLayer from 'ol-stac';
7779
import proj4 from 'proj4';
7880
import proj4list from 'proj4-list';
7981
import * as React from 'react';
@@ -82,12 +84,21 @@ import AnnotationFloater from '@/src/annotations/components/AnnotationFloater';
8284
import { CommandIDs } from '@/src/constants';
8385
import { LoadingOverlay } from '@/src/shared/components/loading';
8486
import StatusBar from '@/src/statusbar/StatusBar';
85-
import { isLightTheme, loadFile, throttle } from '@/src/tools';
87+
import { debounce, isLightTheme, loadFile, throttle } from '@/src/tools';
8688
import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers';
8789
import { FollowIndicator } from './FollowIndicator';
8890
import TemporalSlider from './TemporalSlider';
8991
import { MainViewModel } from './mainviewmodel';
9092

93+
type OlLayerTypes =
94+
| TileLayer
95+
| VectorLayer
96+
| VectorTileLayer
97+
| WebGlTileLayer
98+
| WebGlTileLayer
99+
| HeatmapLayer
100+
| StacLayer
101+
| ImageLayer<any>;
91102
interface IProps {
92103
viewModel: MainViewModel;
93104
}
@@ -205,6 +216,7 @@ export class MainView extends React.Component<IProps, IStates> {
205216
this._contextMenu = new ContextMenu({
206217
commands: this._commands,
207218
});
219+
this._updateCenter = debounce(this.updateCenter, 100);
208220
}
209221

210222
async componentDidMount(): Promise<void> {
@@ -302,6 +314,8 @@ export class MainView extends React.Component<IProps, IStates> {
302314

303315
const view = this._Map.getView();
304316

317+
view.on('change:center', () => this._updateCenter());
318+
305319
// TODO: Note for the future, will need to update listeners if view changes
306320
view.on(
307321
'change:center',
@@ -409,6 +423,22 @@ export class MainView extends React.Component<IProps, IStates> {
409423
}
410424
}
411425

426+
updateCenter = () => {
427+
const extentIn4326 = this.getViewBbox();
428+
this._model.updateBboxSignal.emit(extentIn4326);
429+
};
430+
431+
getViewBbox = (targetProjection = 'EPSG:4326') => {
432+
const view = this._Map.getView();
433+
const extent = view.calculateExtent(this._Map.getSize());
434+
435+
if (view.getProjection().getCode() === targetProjection) {
436+
return extent;
437+
}
438+
439+
return transformExtent(extent, view.getProjection(), targetProjection);
440+
};
441+
412442
createSelectInteraction = () => {
413443
const pointStyle = new Style({
414444
image: new Circle({
@@ -870,24 +900,28 @@ export class MainView extends React.Component<IProps, IStates> {
870900
private async _buildMapLayer(
871901
id: string,
872902
layer: IJGISLayer,
873-
): Promise<Layer | undefined> {
874-
const sourceId = layer.parameters?.source;
875-
const source = this._model.sharedModel.getLayerSource(sourceId);
876-
if (!source) {
877-
return;
878-
}
879-
903+
): Promise<Layer | StacLayer | undefined> {
880904
this.setState(old => ({ ...old, loadingLayer: true }));
881905
this._loadingLayers.add(id);
882906

883-
if (!this._sources[sourceId]) {
884-
await this.addSource(sourceId, source);
885-
}
907+
let newMapLayer: OlLayerTypes;
908+
let layerParameters: any;
909+
let sourceId: string | undefined;
910+
let source: IJGISSource | undefined;
886911

887-
this._loadingLayers.add(id);
888-
889-
let newMapLayer;
890-
let layerParameters;
912+
if (layer.type !== 'StacLayer') {
913+
sourceId = layer.parameters?.source;
914+
if (!sourceId) {
915+
return;
916+
}
917+
source = this._model.sharedModel.getLayerSource(sourceId);
918+
if (!source) {
919+
return;
920+
}
921+
if (!this._sources[sourceId]) {
922+
await this.addSource(sourceId, source);
923+
}
924+
}
891925

892926
// TODO: OpenLayers provides a bunch of sources for specific tile
893927
// providers, so maybe set up some way to use those
@@ -977,22 +1011,46 @@ export class MainView extends React.Component<IProps, IStates> {
9771011
radius: layerParameters.radius ?? 8,
9781012
gradient: layerParameters.color,
9791013
});
1014+
9801015
break;
9811016
}
982-
}
1017+
case 'StacLayer': {
1018+
layerParameters = layer.parameters as IStacLayer;
9831019

984-
await this._waitForSourceReady(newMapLayer);
1020+
newMapLayer = new StacLayer({
1021+
displayPreview: true,
1022+
data: layerParameters.data,
1023+
opacity: layerParameters.opacity,
1024+
visible: layer.visible,
1025+
assets: Object.keys(layerParameters.data.assets),
1026+
extent: layerParameters.data.bbox,
1027+
});
1028+
1029+
this.setState(old => ({
1030+
...old,
1031+
metadata: layerParameters.data.properties,
1032+
}));
1033+
1034+
break;
1035+
}
1036+
}
9851037

9861038
// OpenLayers doesn't have name/id field so add it
9871039
newMapLayer.set('id', id);
9881040

989-
// we need to keep track of which source has which layers
990-
this._sourceToLayerMap.set(layerParameters.source, id);
1041+
// STAC layers don't have source
1042+
if (newMapLayer instanceof Layer) {
1043+
// we need to keep track of which source has which layers
1044+
// Only set sourceToLayerMap if 'source' exists on layerParameters
1045+
if ('source' in layerParameters) {
1046+
this._sourceToLayerMap.set(layerParameters.source, id);
1047+
}
9911048

992-
this.addProjection(newMapLayer);
1049+
this.addProjection(newMapLayer);
1050+
await this._waitForSourceReady(newMapLayer);
1051+
}
9931052

9941053
this._loadingLayers.delete(id);
995-
9961054
return newMapLayer;
9971055
}
9981056

@@ -1005,7 +1063,7 @@ export class MainView extends React.Component<IProps, IStates> {
10051063

10061064
const projectionCode = sourceProjection.getCode();
10071065

1008-
const isProjectionRegistered = getRegisteredProjection(projectionCode);
1066+
const isProjectionRegistered = getProjection(projectionCode);
10091067
if (!isProjectionRegistered) {
10101068
// Check if the projection exists in proj4list
10111069
if (!proj4list[projectionCode]) {
@@ -1195,16 +1253,6 @@ export class MainView extends React.Component<IProps, IStates> {
11951253
mapLayer: Layer,
11961254
oldLayer?: IDict,
11971255
): Promise<void> {
1198-
const sourceId = layer.parameters?.source;
1199-
const source = this._model.sharedModel.getLayerSource(sourceId);
1200-
if (!source) {
1201-
return;
1202-
}
1203-
1204-
if (!this._sources[sourceId]) {
1205-
await this.addSource(sourceId, source);
1206-
}
1207-
12081256
mapLayer.setVisible(layer.visible);
12091257

12101258
switch (layer.type) {
@@ -1266,6 +1314,9 @@ export class MainView extends React.Component<IProps, IStates> {
12661314

12671315
break;
12681316
}
1317+
case 'StacLayer':
1318+
mapLayer.setOpacity(layer.parameters?.opacity || 1);
1319+
break;
12691320
}
12701321
}
12711322

@@ -1456,7 +1507,7 @@ export class MainView extends React.Component<IProps, IStates> {
14561507
* Wait for a layers source state to be 'ready'
14571508
* @param layer The Layer to check
14581509
*/
1459-
private _waitForSourceReady(layer: Layer) {
1510+
private _waitForSourceReady(layer: Layer | LayerGroup) {
14601511
return new Promise<void>((resolve, reject) => {
14611512
const checkState = () => {
14621513
const state = layer.getSourceState();
@@ -1771,10 +1822,6 @@ export class MainView extends React.Component<IProps, IStates> {
17711822
const mapLayer = this.getLayer(id);
17721823
const layerTree = JupyterGISModel.getOrderedLayerIds(this._model);
17731824

1774-
if (!mapLayer) {
1775-
return;
1776-
}
1777-
17781825
if (layerTree.includes(id)) {
17791826
this.updateLayer(id, newLayer, mapLayer, oldLayer);
17801827
} else {
@@ -1914,6 +1961,10 @@ export class MainView extends React.Component<IProps, IStates> {
19141961
extent = tileGrid?.getExtent();
19151962
}
19161963

1964+
if (layer instanceof StacLayer) {
1965+
extent = layer.getExtent();
1966+
}
1967+
19171968
if (!extent) {
19181969
console.warn('Layer has no extent.');
19191970
return;
@@ -2165,4 +2216,5 @@ export class MainView extends React.Component<IProps, IStates> {
21652216
private _loadingLayers: Set<string>;
21662217
private _originalFeatures: IDict<Feature<Geometry>[]> = {};
21672218
private _highlightLayer: VectorLayer<VectorSource>;
2219+
private _updateCenter: CallableFunction;
21682220
}

packages/base/src/panelview/leftpanel.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Message } from '@lumino/messaging';
1212
import { MouseEvent as ReactMouseEvent } from 'react';
1313

1414
import { CommandIDs } from '@/src/constants';
15+
import StacPanel from '@/src/stacBrowser/StacPanel';
1516
import { IControlPanelModel } from '@/src/types';
1617
import { FilterPanel } from './components/filter-panel/Filter';
1718
import { LayersPanel } from './components/layers';
@@ -55,10 +56,20 @@ export class LeftPanelWidget extends SidePanel {
5556
state: this._state,
5657
onSelect: this._onSelect,
5758
});
59+
5860
layerTree.title.caption = 'Layer tree';
5961
layerTree.title.label = 'Layers';
6062
this.addWidget(layerTree);
6163

64+
const stacPanel = new StacPanel({
65+
model: this._model,
66+
tracker: options.tracker,
67+
});
68+
69+
stacPanel.title.caption = 'STAC';
70+
stacPanel.title.label = 'STAC';
71+
this.addWidget(stacPanel);
72+
6273
const filterPanel = new FilterPanel({
6374
model: this._model,
6475
tracker: options.tracker,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from 'react';
2+
3+
interface IBadgeProps extends React.HTMLAttributes<HTMLButtonElement> {
4+
variant?: 'destructive' | 'outline' | 'secondary';
5+
size?: 'sm' | 'lg' | 'icon';
6+
}
7+
8+
function Badge({ variant, ...props }: IBadgeProps) {
9+
return (
10+
// @ts-expect-error lol
11+
<div data-variant={variant} className={'jgis-badge'} {...props} />
12+
);
13+
}
14+
15+
export default Badge;

packages/base/src/shared/components/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(
1414
<Comp
1515
data-size={size}
1616
data-variant={variant}
17-
className={`Button ${className ? className : ''}`}
17+
className={`jgis-button ${className ? className : ''}`}
1818
ref={ref}
1919
{...props}
2020
/>

0 commit comments

Comments
 (0)