Skip to content

Commit 0c8c859

Browse files
igorDykhtaIhor Dykhta
andauthored
feat(raster-tile): Support STAC 1.1.0 core bands, description fallback, and tile debug borders (#3366)
* feat(raster-tile): Support STAC 1.1.0 core bands, description fallback, and tile debug borders Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * improvements Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * docs update; add data update Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * fixes Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * fix raster tile true color regression for stac items after deckgl upgrade Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * band overrides Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * update example Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * more smaller fixes Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * add debug option - zoom offset for raster tile loading Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * relax ts Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> --------- Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> Co-authored-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
1 parent 04b16d3 commit 0c8c859

13 files changed

Lines changed: 1100 additions & 105 deletions

File tree

docs/user-guides/c-types-of-layers/n-raster-tile-layer.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,28 @@ Use Raster Tile layer to visualize satellite/aerial imagery from raster pmtiles
66
2. Select Raster Tile tileset type.
77
3. Paste URL to the tileset:
88
- pmtiles (raster format): provide a direct HTTPS URL to a .pmtiles file containing raster imagery. Raster pmtiles don't require dedicated raster tile servers, unless you want to use elevation meshes.
9-
- STAC Item/Collection (COGs): provide a HTTPS URL to a STAC Item or Collection (v1.0.0+ with EO + Raster extensions). For this option you need to provide a [compatible raster tile server](https://github.com/igorDykhta/kepler-raster-server).
9+
- COG (.tif): provide a direct HTTPS URL to a Cloud Optimized GeoTIFF. The public [TiTiler](https://titiler.xyz) service is used automatically for metadata and tile serving. Elevation is not supported in this mode.
10+
- STAC Item/Collection (COGs): provide a HTTPS URL to a STAC Item or Collection. Both STAC 1.0.x (with EO + Raster extensions) and STAC 1.1.0+ (with core `bands`) are supported. For this option you need to provide a [compatible raster tile server](https://github.com/igorDykhta/kepler-raster-server).
1011
4. Click Add.
1112
5. Style band selection and opacity as needed in Layers panel.
1213

1314
Important notes for COGs via STAC:
1415

15-
- The STAC Item/Collection must include EO and Raster extensions with `eo:bands` and `raster:bands` .
16+
- **STAC 1.0.x:** The STAC Item/Collection must include EO and Raster extensions with `eo:bands` and `raster:bands`.
17+
- **STAC 1.1.0+:** Items using the core `bands` field (where band metadata lives directly on each asset instead of separate `eo:bands`/`raster:bands` extensions) are also supported. Each band object should include `data_type` and optionally `eo:common_name` and `statistics`.
18+
- Both formats can coexist within a single STAC item (some assets using legacy extensions, others using core `bands`).
1619
- COG assets must be publicly accessible over HTTPS.
1720
- You must run your own raster tile server (e.g., TiTiler). Example implementation that supports collections and elevations: [kepler-raster-server](https://github.com/igorDykhta/kepler-raster-server).
1821

22+
# Loading standalone COG (.tif) files
23+
24+
You can load a Cloud Optimized GeoTIFF directly by pasting its URL (ending in `.tif` or `.tiff`) into the "Tileset metadata URL" field. When a COG URL is detected, kepler.gl automatically fetches STAC metadata from the public [TiTiler](https://titiler.xyz) service (`/cog/stac` endpoint) and sets `https://titiler.xyz` as the raster tile server.
25+
26+
- **No self-hosted server required** — the public TiTiler instance handles both metadata and tile serving.
27+
- **Elevation is not available** when using the public TiTiler service.
28+
- The COG file must be publicly accessible over HTTPS.
29+
- If the "Raster tile servers" field is empty when you paste a `.tif` URL, it will be auto-filled with `https://titiler.xyz`.
30+
1931
# Elevation
2032

2133
To enable elevation rendering, you must provide one or more compatible raster tile servers when adding the tileset. Enter them in the "Raster tile servers" field of the Add Tileset form.

src/components/src/modals/tilesets-modals/tileset-raster-form.tsx

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,46 @@ const TilesetInputDescription = styled.div`
3030
font-size: 11px;
3131
`;
3232

33+
const ExampleUrlsContainer = styled.div`
34+
text-align: left;
35+
color: ${props => props.theme.AZURE200};
36+
font-size: 11px;
37+
`;
38+
39+
const ExampleTabs = styled.div`
40+
display: flex;
41+
gap: 6px;
42+
margin-top: 6px;
43+
margin-bottom: 6px;
44+
flex-wrap: wrap;
45+
`;
46+
47+
const ExampleTab = styled.div<{active: boolean}>`
48+
padding: 3px 8px;
49+
border-radius: 3px;
50+
cursor: pointer;
51+
font-size: 11px;
52+
white-space: nowrap;
53+
background: ${props => (props.active ? props.theme.AZURE400 : 'transparent')};
54+
color: ${props => (props.active ? props.theme.WHITE : props.theme.AZURE200)};
55+
border: 1px solid ${props => props.theme.AZURE400};
56+
57+
&:hover {
58+
background: ${props => (props.active ? props.theme.AZURE400 : props.theme.AZURE500)};
59+
}
60+
`;
61+
62+
const ExampleUrl = styled.div`
63+
word-break: break-all;
64+
cursor: pointer;
65+
color: ${props => props.theme.AZURE200};
66+
font-size: 11px;
67+
68+
&:hover {
69+
color: ${props => props.theme.AZURE100};
70+
}
71+
`;
72+
3373
const LabelRow = styled.div`
3474
display: flex;
3575
align-items: center;
@@ -90,6 +130,46 @@ const InfoIconLink = styled.a`
90130
const RASTER_TILE_DOCUMENTATION_URL =
91131
'https://docs.kepler.gl/docs/user-guides/c-types-of-layers/n-raster-tile-layer';
92132

133+
const TITILER_BASE_URL = 'https://titiler.xyz';
134+
135+
function isCOGUrl(url: string): boolean {
136+
if (!url) return false;
137+
try {
138+
const pathname = new URL(url).pathname.toLowerCase().replace(/\/+$/, '');
139+
return pathname.endsWith('.tif') || pathname.endsWith('.tiff');
140+
} catch {
141+
const cleaned = url.trim().toLowerCase().split(/[?#]/)[0].replace(/\/+$/, '');
142+
return cleaned.endsWith('.tif') || cleaned.endsWith('.tiff');
143+
}
144+
}
145+
146+
function getCOGMetadataUrl(cogUrl: string): string {
147+
return `${TITILER_BASE_URL}/cog/stac?url=${encodeURIComponent(cogUrl)}`;
148+
}
149+
150+
const RASTER_TILE_EXAMPLES = [
151+
{
152+
label: 'COG',
153+
name: 'Africa Farms',
154+
url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/TCI.tif'
155+
},
156+
{
157+
label: 'PMTiles',
158+
name: 'Mt Whitney',
159+
url: 'https://pmtiles.io/usgs-mt-whitney-8-15-webp-512.pmtiles'
160+
},
161+
{
162+
label: 'PMTiles',
163+
name: 'Swiss Historical',
164+
url: 'https://public-bucket-for-tests.s3.us-east-1.amazonaws.com/historic-swis-18xx.pmtiles'
165+
},
166+
{
167+
label: 'STAC Collection',
168+
name: 'Sentinel-2 L1C',
169+
url: 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l1c'
170+
}
171+
];
172+
93173
const parseMetadataAllowCollections = (
94174
metadata: JsonObjectOrArray | PMTilesMetadata,
95175
{metadataUrl, rasterTileType}: {metadataUrl: string; rasterTileType: RasterTileType}
@@ -108,10 +188,32 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
108188
const [rasterTileServerUrls, setRasterTileServerUrls] = useState<string>(
109189
(getApplicationConfig().rasterServerUrls || []).join(',')
110190
);
191+
const [exampleTab, setExampleTab] = useState(0);
111192

112193
// Remove trailing slash to prevent issues with raster tile servers
113194
const clearedMetadataUrl = metadataUrl.endsWith('/') ? metadataUrl.slice(0, -1) : metadataUrl;
114195

196+
const isCOG = isCOGUrl(clearedMetadataUrl);
197+
const effectiveMetadataUrl = isCOG ? getCOGMetadataUrl(clearedMetadataUrl) : clearedMetadataUrl;
198+
const effectiveRasterTileServerUrls = isCOG ? TITILER_BASE_URL : rasterTileServerUrls;
199+
200+
const defaultServerUrls = (getApplicationConfig().rasterServerUrls || []).join(',');
201+
202+
const onExampleClick = useCallback(
203+
(url: string, name: string) => {
204+
setMetadataUrl(url);
205+
setTileName(name);
206+
setTileNameWasModified(false);
207+
208+
if (isCOGUrl(url)) {
209+
setRasterTileServerUrls(TITILER_BASE_URL);
210+
} else {
211+
setRasterTileServerUrls(defaultServerUrls);
212+
}
213+
},
214+
[defaultServerUrls]
215+
);
216+
115217
const onTileNameChange = useCallback(
116218
(event: React.ChangeEvent<HTMLInputElement>) => {
117219
event.preventDefault();
@@ -127,11 +229,15 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
127229
const {value} = event.target;
128230
setMetadataUrl(value);
129231

232+
if (isCOGUrl(value) && !rasterTileServerUrls.trim()) {
233+
setRasterTileServerUrls(TITILER_BASE_URL);
234+
}
235+
130236
if (!tileNameWasModified) {
131237
setTileName(value.split('/').filter(Boolean).pop() || '');
132238
}
133239
},
134-
[tileNameWasModified]
240+
[tileNameWasModified, rasterTileServerUrls]
135241
);
136242

137243
const onRasterTileServerUrlsChange = useCallback(
@@ -147,7 +253,7 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
147253
loading,
148254
error: metaError
149255
} = useFetchJson({
150-
url: clearedMetadataUrl,
256+
url: effectiveMetadataUrl,
151257
rasterTileType: isPMTilesUrl(clearedMetadataUrl) ? RasterTileType.PMTILES : RasterTileType.STAC,
152258
process: parseMetadataAllowCollections
153259
});
@@ -173,7 +279,7 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
173279
!error
174280
// We still need raster tile servers for PMTiles when we plan to use elevation
175281
) {
176-
rasterTileServers = rasterTileServerUrls
282+
rasterTileServers = effectiveRasterTileServerUrls
177283
.split(',')
178284
.map(server => server.trim())
179285
.filter(server => server);
@@ -195,7 +301,7 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
195301

196302
const dataset = getDatasetAttributesFromRasterTile({
197303
name: tileName,
198-
metadataUrl: clearedMetadataUrl,
304+
metadataUrl: effectiveMetadataUrl,
199305
rasterTileServerUrls: rasterTileServers
200306
});
201307

@@ -219,7 +325,8 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
219325
metaError,
220326
tileName,
221327
clearedMetadataUrl,
222-
rasterTileServerUrls,
328+
effectiveMetadataUrl,
329+
effectiveRasterTileServerUrls,
223330
setResponse
224331
]);
225332

@@ -255,7 +362,7 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
255362
onChange={onMetadataUrlChange}
256363
/>
257364
<TilesetInputDescription>
258-
Supports raster .pmtiles. Limited support for STAC Items and Collections.
365+
Supports raster .pmtiles, COG (.tif) URLs, STAC Items and Collections.
259366
</TilesetInputDescription>
260367
</div>
261368
{showServerInput && (
@@ -282,6 +389,35 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
282389
</TilesetInputDescription>
283390
</div>
284391
)}
392+
<div>
393+
<TilesetInputDescription>For example, try a public raster tileset:</TilesetInputDescription>
394+
<ExampleUrlsContainer>
395+
<ExampleTabs>
396+
{RASTER_TILE_EXAMPLES.map((ex, i) => (
397+
<ExampleTab
398+
key={`${ex.label}-${ex.name}`}
399+
active={exampleTab === i}
400+
onClick={() => {
401+
setExampleTab(i);
402+
onExampleClick(ex.url, ex.name);
403+
}}
404+
>
405+
{ex.name}
406+
</ExampleTab>
407+
))}
408+
</ExampleTabs>
409+
<ExampleUrl
410+
onClick={() =>
411+
onExampleClick(
412+
RASTER_TILE_EXAMPLES[exampleTab].url,
413+
RASTER_TILE_EXAMPLES[exampleTab].name
414+
)
415+
}
416+
>
417+
{RASTER_TILE_EXAMPLES[exampleTab].url}
418+
</ExampleUrl>
419+
</ExampleUrlsContainer>
420+
</div>
285421
</TilesetInputContainer>
286422
);
287423
};

0 commit comments

Comments
 (0)