Skip to content

Commit d650f9d

Browse files
#778 Support TiTiler expressions (#779)
1 parent 5a20f7d commit d650f9d

7 files changed

Lines changed: 518 additions & 124 deletions

File tree

configure/src/metaconfigs/layer-image-config.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@
158158
"type": "colordropdown",
159159
"width": 3,
160160
"options": "{{COLORMAP_NAMES}}"
161+
},
162+
{
163+
"field": "cogExpression",
164+
"name": "Band Math Expression",
165+
"description": "TiTiler/rio-tiler band math expression. Uses standard math operators (+-/*) where asset_bX represents band X of an asset (e.g., 'asset_b1*2', '(asset_b1+asset_b2)/2'). For RGB output, use semicolons (e.g., 'asset_b1;asset_b2;asset_b3'). For multiple STAC assets, use different asset names (e.g., 'assetname1_b1 + assetname2_b1'). If you omit the asset prefix (e.g., 'b1'), it will automatically default to 'asset_b1'. When specified, this replaces the 'Tile Bands' parameter.",
166+
"type": "textnotrim",
167+
"width": 6
168+
},
169+
{
170+
"field": "cogExpressionEditable",
171+
"name": "Allow User Expression Editing",
172+
"description": "If true, users can modify the band math expression in the LayersTool settings panel.",
173+
"type": "switch",
174+
"width": 3,
175+
"defaultChecked": false
161176
}
162177
]
163178
},

configure/src/metaconfigs/layer-tile-config.json

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,56 @@
132132
]
133133
},
134134
{
135-
"name": "Cloud-Optimized GeoTiffs (COG)",
135+
"name": "Digital Elevation Model (DEM) Tiles",
136+
"components": [
137+
{
138+
"field": "demtileurl",
139+
"name": "DEM Tile URL",
140+
"description": "A file path like URL but pointing to a Digital Elevation Map tileset generated by auxillary/1bto4b/rasterstotiles1bto4b.py This is responsible for 3D data in the globe. It would be ideal if this tileset can match the extents of its corresponding raster and has either no nodata or has nodata far lower than that of its lowest point.",
141+
"type": "text",
142+
"width": 10
143+
},
144+
{
145+
"field": "demparser",
146+
"name": "DEM Parser",
147+
"description": "",
148+
"type": "dropdown",
149+
"width": 2,
150+
"options": ["rgba", "tif"]
151+
}
152+
]
153+
},
154+
{
155+
"name": "Other",
156+
"components": [
157+
{
158+
"field": "boundingBox",
159+
"name": "Bounding Box",
160+
"description": "minx,miny,maxx,maxy",
161+
"type": "textarray",
162+
"width": 12
163+
}
164+
]
165+
},
166+
{
167+
"name": "Actions",
168+
"components": [
169+
{
170+
"name": "Populate Fields From tilemapresource.xml or from cog/info",
171+
"description": "If the above URL is relative to the Missions/{mission} directory and the tileset contains a tilemapresource.xml within it, queries that xml and auto-fills the 'Minimum Zoom', 'Maximum Native Zoom' and 'Bounding Box' fields above. If it is a COG and TiTiler is true, the COG's data will be queried instead.",
172+
"type": "button",
173+
"action": "tile-populate-from-x",
174+
"width": 6
175+
}
176+
]
177+
}
178+
]
179+
},
180+
{
181+
"name": "COG",
182+
"rows": [
183+
{
184+
"name": "TiTiler Configuration",
136185
"components": [
137186
{
138187
"field": "throughTileServer",
@@ -171,11 +220,12 @@
171220
]
172221
},
173222
{
223+
"name": "Bands",
174224
"components": [
175225
{
176226
"field": "cogBands",
177227
"name": "Tile Bands",
178-
"description": "Which bands from the COG from which to generate tiles. Defaults to '1,2,3' as RGB or '1' if it's a Transformed 32-bit COG. Can be a single number or a comma-separated list of numbers. Order matters.",
228+
"description": "Which bands from the COG from which to generate tiles. Defaults to '1,2,3' as RGB or '1' if it's a Transformed Single Data Band COG. Can be a single number or a comma-separated list of numbers. Order matters.",
179229
"type": "textarray",
180230
"width": 4
181231
},
@@ -189,27 +239,47 @@
189239
]
190240
},
191241
{
192-
"subname": "32-bit COGs",
242+
"subname": "Band Math Expression",
243+
"components": [
244+
{
245+
"field": "cogExpression",
246+
"name": "Band Math Expression",
247+
"description": "TiTiler/rio-tiler band math expression. Uses standard math operators (+-/*) where asset_bX represents band X of an asset (e.g., 'asset_b1*2', '(asset_b1+asset_b2)/2'). For RGB output, use semicolons (e.g., 'asset_b1;asset_b2;asset_b3'). For multiple STAC assets, use different asset names (e.g., 'assetname1_b1 + assetname2_b1'). If you omit the asset prefix (e.g., 'b1'), it will automatically default to 'asset_b1'. When specified, this replaces the 'Tile Bands' parameter.",
248+
"type": "textnotrim",
249+
"width": 9
250+
},
251+
{
252+
"field": "cogExpressionEditable",
253+
"name": "Allow User Expression Editing",
254+
"description": "If true, users can modify the band math expression in the LayersTool settings panel.",
255+
"type": "switch",
256+
"width": 3,
257+
"defaultChecked": false
258+
}
259+
]
260+
},
261+
{
262+
"name": "Single Data Band COG Transformation",
193263
"components": [
194264
{
195265
"field": "cogTransform",
196-
"name": "Transform 32-bit COG",
197-
"description": "Enable rescaling and coloring 32-bit COGs on the fly. Will use TiTiler.",
266+
"name": "Transform Single Data Band COG",
267+
"description": "Enable rescaling and coloring Single Data Band COGs on the fly. Will use TiTiler.",
198268
"type": "switch",
199269
"width": 3,
200270
"defaultChecked": false
201271
},
202272
{
203273
"field": "cogMin",
204274
"name": "Minimum Pixel Data Value",
205-
"description": "If using TiTiler, STAC and 32-bit COGs, the default minimum value for which to rescale.",
275+
"description": "If using TiTiler, STAC and Single Data Band COGs, the default minimum value for which to rescale.",
206276
"type": "number",
207277
"width": 2
208278
},
209279
{
210280
"field": "cogMax",
211281
"name": "Maximum Pixel Data Value",
212-
"description": "If using TiTiler, STAC and 32-bit COGs, the default maximum value for which to rescale.",
282+
"description": "If using TiTiler, STAC and Single Data Band COGs, the default maximum value for which to rescale.",
213283
"type": "number",
214284
"width": 2
215285
},
@@ -229,50 +299,6 @@
229299
"options": "{{COLORMAP_NAMES}}"
230300
}
231301
]
232-
},
233-
{
234-
"name": "Digital Elevation Model (DEM) Tiles",
235-
"components": [
236-
{
237-
"field": "demtileurl",
238-
"name": "DEM Tile URL",
239-
"description": "A file path like URL but pointing to a Digital Elevation Map tileset generated by auxillary/1bto4b/rasterstotiles1bto4b.py This is responsible for 3D data in the globe. It would be ideal if this tileset can match the extents of its corresponding raster and has either no nodata or has nodata far lower than that of its lowest point.",
240-
"type": "text",
241-
"width": 10
242-
},
243-
{
244-
"field": "demparser",
245-
"name": "DEM Parser",
246-
"description": "",
247-
"type": "dropdown",
248-
"width": 2,
249-
"options": ["rgba", "tif"]
250-
}
251-
]
252-
},
253-
{
254-
"name": "Other",
255-
"components": [
256-
{
257-
"field": "boundingBox",
258-
"name": "Bounding Box",
259-
"description": "minx,miny,maxx,maxy",
260-
"type": "textarray",
261-
"width": 12
262-
}
263-
]
264-
},
265-
{
266-
"name": "Actions",
267-
"components": [
268-
{
269-
"name": "Populate Fields From tilemapresource.xml or from cog/info",
270-
"description": "If the above URL is relative to the Missions/{mission} directory and the tileset contains a tilemapresource.xml within it, queries that xml and auto-fills the 'Minimum Zoom', 'Maximum Native Zoom' and 'Bounding Box' fields above. If it is a COG and TiTiler is true, the COG's data will be queried instead.",
271-
"type": "button",
272-
"action": "tile-populate-from-x",
273-
"width": 6
274-
}
275-
]
276302
}
277303
]
278304
},

src/essence/Basics/Layers_/leaflet-tilelayer-middleware.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ var colorFilterExtension = {
6464
}
6565
}
6666

67+
// Add expression parameter if it exists (takes precedence over bands)
68+
// Check currentCogExpression first (runtime value), then fall back to cogExpression (configured value)
69+
const expressionToUse = this.options.currentCogExpression || this.options.cogExpression
70+
if (expressionToUse && expressionToUse.trim() !== '') {
71+
// Process expression to add asset_ prefix if needed
72+
const processExpression = (expression) => {
73+
if (!expression || expression.trim() === '') return expression
74+
// Replace bX or BX (where X is a number) with asset_bX or asset_BX
75+
// Only replace if not already prefixed with an asset name (word_bX pattern)
76+
return expression.replace(/(?<!\w)([bB])(\d+)/g, 'asset_$1$2')
77+
}
78+
const processedExpression = processExpression(expressionToUse)
79+
url += `${url.indexOf('?') === -1 ? '?' : '&'}expression=${encodeURIComponent(processedExpression)}`
80+
}
81+
6782
if (mmgisglobal.options?.stac?.mosaicItemLimit != null) {
6883
url += `${url.indexOf('?') === -1 ? '?' : '&'}items_limit=${
6984
mmgisglobal.options.stac.mosaicItemLimit

src/essence/Basics/Map_/Map_.js

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,14 @@ async function makeVelocityLayer(
11591159
}
11601160

11611161
async function makeTileLayer(layerObj) {
1162+
// Helper function to add default 'asset_' prefix to bands in expressions if not already prefixed
1163+
const processExpression = (expression) => {
1164+
if (!expression || expression.trim() === '') return expression
1165+
// Replace bX or BX (where X is a number) with asset_bX or asset_BX
1166+
// Only replace if not already prefixed with an asset name (word_bX pattern)
1167+
return expression.replace(/(?<!\w)([bB])(\d+)/g, 'asset_$1$2')
1168+
}
1169+
11621170
let layerUrl = L_.getUrl(layerObj.type, layerObj.url, layerObj)
11631171

11641172
let splitColonType
@@ -1173,13 +1181,17 @@ async function makeTileLayer(layerObj) {
11731181
splitColonType = splitColonLayerUrl[0]
11741182
const splitParams = splitColonLayerUrl[1].split('?')
11751183

1176-
// Bands
1177-
bandsParam = ''
1178-
b = layerObj.cogBands
1179-
if (b != null) {
1180-
b.forEach((band) => {
1181-
if (band != null) bandsParam += `&bidx=${band}`
1182-
})
1184+
// Bands parameter (expression will be added dynamically in getTileUrl)
1185+
let bandsParamStac = ''
1186+
1187+
// Only add bands if no expression exists (expression takes precedence)
1188+
if (!layerObj.cogExpression || layerObj.cogExpression.trim() === '') {
1189+
b = layerObj.cogBands
1190+
if (b != null) {
1191+
b.forEach((band) => {
1192+
if (band != null) bandsParamStac += `&bidx=${band}`
1193+
})
1194+
}
11831195
}
11841196

11851197
// Resampling
@@ -1194,18 +1206,23 @@ async function makeTileLayer(layerObj) {
11941206
splitParams[0]
11951207
}/tiles/${
11961208
layerObj.tileMatrixSet || 'WebMercatorQuad'
1197-
}/{z}/{x}/{y}?assets=asset${bandsParam}${resamplingParam}`
1209+
}/{z}/{x}/{y}?assets=asset${bandsParamStac}${resamplingParam}`
11981210
layerObj.tileformat = 'wmts'
11991211
break
12001212
case 'COG':
12011213
splitColonType = splitColonLayerUrl[0]
1202-
// Bands
1214+
1215+
// Bands parameter (expression will be added dynamically in getTileUrl)
12031216
bandsParam = ''
1204-
b = layerObj.cogBands
1205-
if (b != null) {
1206-
b.forEach((band) => {
1207-
if (band != null) bandsParam += `&bidx=${band}`
1208-
})
1217+
1218+
// Only add bands if no expression exists (expression takes precedence)
1219+
if (!layerObj.cogExpression || layerObj.cogExpression.trim() === '') {
1220+
b = layerObj.cogBands
1221+
if (b != null) {
1222+
b.forEach((band) => {
1223+
if (band != null) bandsParam += `&bidx=${band}`
1224+
})
1225+
}
12091226
}
12101227

12111228
resamplingParam = ''
@@ -1274,6 +1291,8 @@ async function makeTileLayer(layerObj) {
12741291
cogMax: layerObj.cogMax,
12751292
currentCogMax: layerObj.currentCogMax,
12761293
cogColormap: layerObj.cogColormap,
1294+
cogExpression: layerObj.cogExpression,
1295+
currentCogExpression: layerObj.currentCogExpression,
12771296
variables: layerObj.variables || {},
12781297
})
12791298

src/essence/Tools/Identifier/IdentifierTool.js

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -692,20 +692,39 @@ function bestMatchInLegend(rgba, legendData) {
692692
}
693693

694694
function queryDataValue(url, lng, lat, numBands, layerUUID, callback) {
695+
// Helper function to add default 'asset_' prefix to bands in expressions if not already prefixed
696+
const processExpression = (expression) => {
697+
if (!expression || expression.trim() === '') return expression
698+
// Replace bX or BX (where X is a number) with asset_bX or asset_BX
699+
// Only replace if not already prefixed with an asset name (word_bX pattern)
700+
return expression.replace(/(?<!\w)([bB])(\d+)/g, 'asset_$1$2')
701+
}
702+
695703
numBands = numBands || 1
696704
var dataPath
697705
if (url != null && url.startsWith('stac-collection:')) {
698706
let timeParam = ''
699707
if (L_.layers.data[layerUUID].time?.enabled == true)
700708
timeParam = `&datetime=${L_.layers.data[layerUUID].time.start}/${L_.layers.data[layerUUID].time.end}`
701709

702-
// Bands
710+
// Expression or Bands
711+
let expressionParam = ''
703712
let bandsParam = ''
704-
let b = L_.layers.data[layerUUID].cogBandsQuery
705-
if (b != null) {
706-
b.forEach((band) => {
707-
if (band != null) bandsParam += `&bidx=${band}`
708-
})
713+
const layer = L_.layers.data[layerUUID]
714+
715+
// Check currentCogExpression first (runtime value), then fall back to cogExpression (configured value)
716+
const expressionToUse = layer.currentCogExpression || layer.cogExpression
717+
if (expressionToUse && expressionToUse.trim() !== '') {
718+
const processedExpression = processExpression(expressionToUse)
719+
expressionParam = `&expression=${encodeURIComponent(processedExpression)}`
720+
} else {
721+
// Fall back to bands if no expression
722+
let b = layer.cogBandsQuery
723+
if (b != null) {
724+
b.forEach((band) => {
725+
if (band != null) bandsParam += `&bidx=${band}`
726+
})
727+
}
709728
}
710729

711730
fetch(
@@ -717,7 +736,7 @@ function queryDataValue(url, lng, lat, numBands, layerUUID, callback) {
717736
).replace(/\/$/g, '')}`
718737
}/titilerpgstac/collections/${
719738
url.split('stac-collection:')[1]
720-
}/point/${lng},${lat}?assets=asset&items_limit=10${timeParam}${bandsParam}`,
739+
}/point/${lng},${lat}?assets=asset&items_limit=10${timeParam}${expressionParam}${bandsParam}`,
721740
{
722741
method: 'GET',
723742
headers: {
@@ -753,15 +772,24 @@ function queryDataValue(url, lng, lat, numBands, layerUUID, callback) {
753772
if (L_.layers.data[layerUUID].time?.enabled == true)
754773
timeParam = `&datetime=${L_.layers.data[layerUUID].time.start}/${L_.layers.data[layerUUID].time.end}`
755774

756-
// Bands
775+
// Expression or Bands
776+
let expressionParam = ''
757777
let bandsParam = ''
758-
let b =
759-
L_.layers.data[layerUUID].cogBandsQuery ||
760-
L_.layers.data[layerUUID].cogBands
761-
if (b != null) {
762-
b.forEach((band) => {
763-
if (band != null) bandsParam += `&bidx=${band}`
764-
})
778+
const layer = L_.layers.data[layerUUID]
779+
780+
// Check currentCogExpression first (runtime value), then fall back to cogExpression (configured value)
781+
const expressionToUse = layer.currentCogExpression || layer.cogExpression
782+
if (expressionToUse && expressionToUse.trim() !== '') {
783+
const processedExpression = processExpression(expressionToUse)
784+
expressionParam = `&expression=${encodeURIComponent(processedExpression)}`
785+
} else {
786+
// Fall back to bands if no expression
787+
let b = layer.cogBandsQuery || layer.cogBands
788+
if (b != null) {
789+
b.forEach((band) => {
790+
if (band != null) bandsParam += `&bidx=${band}`
791+
})
792+
}
765793
}
766794

767795
fetch(
@@ -774,7 +802,7 @@ function queryDataValue(url, lng, lat, numBands, layerUUID, callback) {
774802
}/titiler/cog/point/${lng},${lat}?assets=asset&url=${L_.getUrl(
775803
'tile',
776804
url
777-
)}${timeParam}${bandsParam}`,
805+
)}${timeParam}${expressionParam}${bandsParam}`,
778806
{
779807
method: 'GET',
780808
headers: {

0 commit comments

Comments
 (0)