Skip to content

Commit 7db758d

Browse files
authored
Merge pull request #11 from boettiger-lab/feat/alias-and-default-filter
feat: alias + default_filter for config-driven layer splitting/filtering
2 parents e870cc4 + adb41a1 commit 7db758d

File tree

4 files changed

+150
-57
lines changed

4 files changed

+150
-57
lines changed

app/dataset-catalog.js

Lines changed: 86 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,19 @@ export class DatasetCatalog {
8585
* Process a single STAC collection into a DatasetEntry.
8686
*/
8787
processCollection(collection, options = {}) {
88-
// Build asset allowlist and per-asset overrides from config
89-
let allowedAssets = null;
90-
const assetOptions = new Map();
88+
// Build ordered asset config list.
89+
// Using an array (not a Map) so the same STAC asset can appear multiple times
90+
// under different aliases — e.g. one PMTiles split into "fee" and "easement" layers
91+
// via { id: "pmtiles", alias: "fee", ... } and { id: "pmtiles", alias: "easement", ... }.
92+
let assetConfigList = null;
9193
if (Array.isArray(options.assets)) {
92-
allowedAssets = new Set();
94+
assetConfigList = [];
9395
for (const a of options.assets) {
9496
if (typeof a === 'string') {
95-
allowedAssets.add(a);
97+
assetConfigList.push({ key: a, assetId: a, config: {} });
9698
} else if (a && a.id) {
97-
allowedAssets.add(a.id);
98-
assetOptions.set(a.id, a);
99+
const key = a.alias || a.id;
100+
assetConfigList.push({ key, assetId: a.id, config: a });
99101
}
100102
}
101103
}
@@ -114,7 +116,7 @@ export class DatasetCatalog {
114116
columns: this.extractColumns(collection),
115117

116118
// Visual assets (for map display) — filtered by config
117-
mapLayers: this.extractMapLayers(collection, options, allowedAssets, assetOptions),
119+
mapLayers: this.extractMapLayers(collection, options, assetConfigList),
118120

119121
// Parquet/H3 assets (for SQL via MCP) — always load all
120122
parquetAssets: this.extractParquetAssets(collection),
@@ -132,47 +134,85 @@ export class DatasetCatalog {
132134
/**
133135
* Extract map-displayable assets (PMTiles and COGs).
134136
* Each becomes a potential map layer.
137+
*
138+
* When assetConfigList is provided (filtered mode), iterates the config entries
139+
* in order — supporting multiple logical layers from one STAC asset via alias.
140+
* When null, all visual assets from the STAC collection are included.
141+
*
135142
* @param {Object} collection - STAC collection
136143
* @param {Object} options - Collection-level options
137-
* @param {Set|null} allowedAssets - If set, only include these asset IDs
138-
* @param {Map} assetOptions - Per-asset overrides keyed by asset ID
144+
* @param {Array|null} assetConfigList - Ordered list of {key, assetId, config}
139145
*/
140-
extractMapLayers(collection, options = {}, allowedAssets = null, assetOptions = new Map()) {
146+
extractMapLayers(collection, options = {}, assetConfigList = null) {
141147
const layers = [];
142-
const assets = collection.assets || {};
143-
144-
for (const [assetId, asset] of Object.entries(assets)) {
145-
// Skip if an asset allowlist is specified and this asset isn't in it
146-
if (allowedAssets && !allowedAssets.has(assetId)) continue;
147-
148-
const type = asset.type || '';
149-
const perAsset = assetOptions.get(assetId) || {};
150-
151-
if (type.includes('pmtiles')) {
152-
layers.push({
153-
assetId,
154-
layerType: 'vector',
155-
title: perAsset.display_name || asset.title || assetId,
156-
url: asset.href,
157-
sourceLayer: asset['vector:layers']?.[0] || asset['pmtiles:layer'] || assetId,
158-
description: asset.description || '',
159-
defaultStyle: perAsset.default_style || null,
160-
tooltipFields: perAsset.tooltip_fields || null,
161-
defaultVisible: perAsset.visible === true,
162-
});
163-
} else if (type.includes('geotiff') || type.includes('tiff')) {
164-
const colormap = perAsset.colormap || options.colormap || 'reds';
165-
const rescale = perAsset.rescale || options.rescale || null;
166-
167-
layers.push({
168-
assetId,
169-
layerType: 'raster',
170-
title: perAsset.display_name || asset.title || assetId,
171-
cogUrl: asset.href,
172-
colormap,
173-
rescale,
174-
description: asset.description || '',
175-
});
148+
const stacAssets = collection.assets || {};
149+
150+
if (assetConfigList) {
151+
// Filtered mode: iterate config entries so aliases and ordering are respected
152+
for (const { key, assetId, config } of assetConfigList) {
153+
const asset = stacAssets[assetId];
154+
if (!asset) continue;
155+
156+
const type = asset.type || '';
157+
158+
if (type.includes('pmtiles')) {
159+
layers.push({
160+
assetId: key,
161+
layerType: 'vector',
162+
title: config.display_name || asset.title || assetId,
163+
url: asset.href,
164+
sourceLayer: asset['vector:layers']?.[0] || asset['pmtiles:layer'] || assetId,
165+
description: asset.description || '',
166+
defaultStyle: config.default_style || null,
167+
tooltipFields: config.tooltip_fields || null,
168+
defaultVisible: config.visible === true,
169+
defaultFilter: config.default_filter || null,
170+
});
171+
} else if (type.includes('geotiff') || type.includes('tiff')) {
172+
layers.push({
173+
assetId: key,
174+
layerType: 'raster',
175+
title: config.display_name || asset.title || assetId,
176+
cogUrl: asset.href,
177+
colormap: config.colormap || options.colormap || 'reds',
178+
rescale: config.rescale || options.rescale || null,
179+
description: asset.description || '',
180+
defaultVisible: config.visible === true,
181+
defaultFilter: config.default_filter || null,
182+
});
183+
}
184+
}
185+
} else {
186+
// Unfiltered mode: include all visual assets from the STAC collection
187+
for (const [assetId, asset] of Object.entries(stacAssets)) {
188+
const type = asset.type || '';
189+
190+
if (type.includes('pmtiles')) {
191+
layers.push({
192+
assetId,
193+
layerType: 'vector',
194+
title: asset.title || assetId,
195+
url: asset.href,
196+
sourceLayer: asset['vector:layers']?.[0] || asset['pmtiles:layer'] || assetId,
197+
description: asset.description || '',
198+
defaultStyle: null,
199+
tooltipFields: null,
200+
defaultVisible: false,
201+
defaultFilter: null,
202+
});
203+
} else if (type.includes('geotiff') || type.includes('tiff')) {
204+
layers.push({
205+
assetId,
206+
layerType: 'raster',
207+
title: asset.title || assetId,
208+
cogUrl: asset.href,
209+
colormap: options.colormap || 'reds',
210+
rescale: options.rescale || null,
211+
description: asset.description || '',
212+
defaultVisible: false,
213+
defaultFilter: null,
214+
});
215+
}
176216
}
177217
}
178218

@@ -364,6 +404,7 @@ export class DatasetCatalog {
364404
columns: ds.columns,
365405
tooltipFields: ml.tooltipFields || null,
366406
defaultVisible: ml.defaultVisible || false,
407+
defaultFilter: ml.defaultFilter || null,
367408
});
368409
} else if (ml.layerType === 'raster') {
369410
let tilesUrl = `${this.titilerUrl}/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.png?url=${encodeURIComponent(ml.cogUrl)}&colormap_name=${ml.colormap}`;

app/map-manager.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class MapManager {
4747
},
4848
center: options.center || [-119.4, 36.8],
4949
zoom: options.zoom || 6,
50+
renderWorldCopies: false,
5051
});
5152

5253
this.map.addControl(new maplibregl.NavigationControl(), 'top-left');
@@ -77,7 +78,7 @@ export class MapManager {
7778
* Register a single layer on the map.
7879
*/
7980
registerLayer(config) {
80-
const { layerId, datasetId, displayName, type, source, sourceLayer, paint, columns, tooltipFields, defaultVisible } = config;
81+
const { layerId, datasetId, displayName, type, source, sourceLayer, paint, columns, tooltipFields, defaultVisible, defaultFilter } = config;
8182
const sourceId = `src-${layerId.replace(/\//g, '-')}`;
8283
const mapLayerId = `layer-${layerId.replace(/\//g, '-')}`;
8384

@@ -104,6 +105,11 @@ export class MapManager {
104105

105106
this.map.addLayer(layerDef);
106107

108+
// Apply default filter if declared
109+
if (defaultFilter) {
110+
this.map.setFilter(mapLayerId, defaultFilter);
111+
}
112+
107113
// Store state
108114
this.layers.set(layerId, {
109115
layerId,
@@ -114,7 +120,7 @@ export class MapManager {
114120
type,
115121
sourceLayer: sourceLayer || null,
116122
visible: defaultVisible || false,
117-
filter: null,
123+
filter: defaultFilter || null,
118124
columns: columns || [],
119125
defaultPaint: { ...(paint || {}) },
120126
tooltipFields: tooltipFields || null,

example-ghpages/layers-input.json

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,41 @@
3333
"assets": [
3434
{
3535
"id": "pmtiles",
36-
"display_name": "Protected Areas (PAD-US)",
36+
"alias": "fee",
37+
"display_name": "Protected Areas - Fee (GAP 1 & 2)",
3738
"visible": true,
3839
"default_style": {
3940
"fill-color": ["match", ["get", "GAP_Sts"],
40-
1, "#26633A",
41-
2, "#3E9C47",
42-
3, "#7EB3D3",
43-
4, "#BDBDBD",
41+
"1", "#26633A",
42+
"2", "#3E9C47",
43+
"3", "#7EB3D3",
44+
"4", "#BDBDBD",
4445
"#888888"
4546
],
4647
"fill-opacity": 0.7
4748
},
49+
"default_filter": ["all",
50+
["==", "FeatClass", "Fee"],
51+
["in", "GAP_Sts", "1", "2"]
52+
],
53+
"tooltip_fields": ["Unit_Nm", "GAP_Sts", "Mang_Type"]
54+
},
55+
{
56+
"id": "pmtiles",
57+
"alias": "easement",
58+
"display_name": "Protected Areas - Easement",
59+
"visible": false,
60+
"default_style": {
61+
"fill-color": ["match", ["get", "GAP_Sts"],
62+
"1", "#26633A",
63+
"2", "#3E9C47",
64+
"3", "#7EB3D3",
65+
"4", "#BDBDBD",
66+
"#888888"
67+
],
68+
"fill-opacity": 0.7
69+
},
70+
"default_filter": ["==", "FeatClass", "Easement"],
4871
"tooltip_fields": ["Unit_Nm", "GAP_Sts", "Mang_Type"]
4972
}
5073
]

example-k8s/layers-input.json

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,41 @@
1515
"assets": [
1616
{
1717
"id": "pmtiles",
18-
"display_name": "Protected Areas (PAD-US)",
18+
"alias": "fee",
19+
"display_name": "Protected Areas - Fee (GAP 1 & 2)",
1920
"visible": true,
2021
"default_style": {
2122
"fill-color": ["match", ["get", "GAP_Sts"],
22-
1, "#26633A",
23-
2, "#3E9C47",
24-
3, "#7EB3D3",
25-
4, "#BDBDBD",
23+
"1", "#26633A",
24+
"2", "#3E9C47",
25+
"3", "#7EB3D3",
26+
"4", "#BDBDBD",
2627
"#888888"
2728
],
2829
"fill-opacity": 0.7
2930
},
31+
"default_filter": ["all",
32+
["==", "FeatClass", "Fee"],
33+
["in", "GAP_Sts", "1", "2"]
34+
],
35+
"tooltip_fields": ["Unit_Nm", "GAP_Sts", "Mang_Type"]
36+
},
37+
{
38+
"id": "pmtiles",
39+
"alias": "easement",
40+
"display_name": "Protected Areas - Easement",
41+
"visible": false,
42+
"default_style": {
43+
"fill-color": ["match", ["get", "GAP_Sts"],
44+
"1", "#26633A",
45+
"2", "#3E9C47",
46+
"3", "#7EB3D3",
47+
"4", "#BDBDBD",
48+
"#888888"
49+
],
50+
"fill-opacity": 0.7
51+
},
52+
"default_filter": ["==", "FeatClass", "Easement"],
3053
"tooltip_fields": ["Unit_Nm", "GAP_Sts", "Mang_Type"]
3154
}
3255
]

0 commit comments

Comments
 (0)