From 9099fc418e52e6a73951750be22acee2d8b03664 Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Tue, 14 Jan 2025 11:26:37 -0800 Subject: [PATCH] #613 Legends for Transformed Titiler COGs (#614) * #613 Improved transform COG legend * #613 32bit cog legend and partial id tool * #613 Identifer Tool and other touchups --- .../src/metaconfigs/layer-tile-config.json | 13 +- src/essence/Ancillary/TimeUI.css | 9 +- src/essence/Ancillary/TimeUI.js | 8 +- .../Tools/Identifier/IdentifierTool.js | 250 ++++++++++++------ src/essence/Tools/Layers/LayersTool.css | 59 ++++- src/essence/Tools/Layers/LayersTool.js | 112 +++++++- src/essence/Tools/Legend/LegendTool.js | 38 +-- 7 files changed, 367 insertions(+), 122 deletions(-) diff --git a/configure/src/metaconfigs/layer-tile-config.json b/configure/src/metaconfigs/layer-tile-config.json index 51d60498..af303414 100644 --- a/configure/src/metaconfigs/layer-tile-config.json +++ b/configure/src/metaconfigs/layer-tile-config.json @@ -140,7 +140,7 @@ { "field": "cogTransform", "name": "Transform COG", - "description": "Enable rescaling and coloring 32-bit COGs on the fly.", + "description": "Enable rescaling and coloring 32-bit COGs on the fly. Will use TiTiler.", "type": "switch", "width": 3, "defaultChecked": false @@ -150,14 +150,21 @@ "name": "Minimum Pixel Data Value", "description": "If using TiTiler, STAC and 32-bit COGs, the default minimum value for which to rescale.", "type": "number", - "width": 3 + "width": 2 }, { "field": "cogMax", "name": "Maximum Pixel Data Value", "description": "If using TiTiler, STAC and 32-bit COGs, the default maximum value for which to rescale.", "type": "number", - "width": 3 + "width": 2 + }, + { + "field": "cogUnits", + "name": "Units", + "description": "Units string by which to suffix values. For instance if the units are meters, use 'm' so that values are displayed as '100m'.", + "type": "text", + "width": 2 }, { "field": "cogColormap", diff --git a/src/essence/Ancillary/TimeUI.css b/src/essence/Ancillary/TimeUI.css index 90b2477c..0dbbaf17 100644 --- a/src/essence/Ancillary/TimeUI.css +++ b/src/essence/Ancillary/TimeUI.css @@ -110,7 +110,7 @@ background: var(--color-a2); } #mmgisTimeUITimelineSlider .rangeSlider:after { - content: ""; + content: ''; position: absolute; width: 100%; height: 2px; @@ -219,8 +219,9 @@ pointer-events: none; } #mmgisTimeUITimelineHisto > div { - background: #056e94; - height: 100%; + background: var(--color-c); + height: 50%; + margin-top: 21px; transition: margin-top 0.2s ease-in-out; } @@ -266,4 +267,4 @@ } #mmgisTimeUIEndWrapperFake { right: 0; -} \ No newline at end of file +} diff --git a/src/essence/Ancillary/TimeUI.js b/src/essence/Ancillary/TimeUI.js index f05adc60..941c9d88 100644 --- a/src/essence/Ancillary/TimeUI.js +++ b/src/essence/Ancillary/TimeUI.js @@ -868,7 +868,7 @@ const TimeUI = { const endtimeISO = new Date(TimeUI._timelineEndTimestamp).toISOString() const NUM_BINS = Math.max( - Math.min(endTimestamp - startTimestamp, 360), + Math.min(endTimestamp - startTimestamp, 255), 1 ) let bins = new Array(NUM_BINS).fill(0) @@ -949,9 +949,9 @@ const TimeUI = { histoElm.append( `
` + }%; opacity:${ + (b > 0 ? 20 : 0) + (b / minmax.max) * 80 + }%;">` ) }) } diff --git a/src/essence/Tools/Identifier/IdentifierTool.js b/src/essence/Tools/Identifier/IdentifierTool.js index 6d85cce4..c8c402ca 100644 --- a/src/essence/Tools/Identifier/IdentifierTool.js +++ b/src/essence/Tools/Identifier/IdentifierTool.js @@ -41,6 +41,7 @@ var IdentifierTool = { toolController.style('right', '5px') toolContent.style('left', null) toolContent.style('right', '0px') + toolContent.style('margin-bottom', '5px') } else if ( this.justification !== L_.getToolVars('legend')['justification'] ) { @@ -177,6 +178,11 @@ var IdentifierTool = { Globe_.litho.zoom, ]) }, + clearCursor: function (e) { + clearTimeout(IdentifierTool.mousemoveTimeout) + clearTimeout(IdentifierTool.mousemoveTimeoutMap) + CursorInfo.hide() + }, //lnglatzoom is [lng,lat,zoom] //if trueValue is true, query the data layer for the value, else us the legend if possible idPixel: function (e, lnglatzoom, trueValue, selfish) { @@ -192,11 +198,14 @@ var IdentifierTool = { if (L_.layers.on[n] == true) { //We only want the tile layers if (L_.layers.data[n].type == 'tile') { - let url = L_.getUrl( - L_.layers.data[n].type, - L_.layers.data[n].url, - L_.layers.data[n] - ) + let url = + L_.layers.data[n].url.indexOf('stac-collection:') === 0 + ? L_.layers.data[n].url + : L_.getUrl( + L_.layers.data[n].type, + L_.layers.data[n].url, + L_.layers.data[n] + ) IdentifierTool.activeLayerURLs.push(url) IdentifierTool.activeLayerNames.push(n) IdentifierTool.zoomLevels.push( @@ -211,77 +220,89 @@ var IdentifierTool = { //get the xyz and images of those layers for (var i = 0; i < IdentifierTool.activeLayerURLs.length; i++) { - var activeZ = - IdentifierTool.activeTiles[i] && IdentifierTool.activeTiles[i].z - ? IdentifierTool.activeTiles[i].z - : IdentifierTool.zoomLevels[i] - var az = Math.min(activeZ, IdentifierTool.zoomLevels[i]) - var ax = F_.lon2tileUnfloored(lnglatzoom[0], az) - var ay = F_.lat2tileUnfloored(lnglatzoom[1], az) - var tz = az - var tx = Math.floor(ax) - var ty = Math.floor(ay) - //Invert y - if (IdentifierTool.tileFormats[i] == 'tms') - ty = Math.pow(2, tz) - 1 - ty - - IdentifierTool.currentTiles[i] = { x: tx, y: ty, z: tz } - //Default activeTiles if none; - if (!IdentifierTool.activeTiles[i]) - IdentifierTool.activeTiles[i] = { x: 0, y: 0, z: -1 } - - //pixel on canvas. decimal part * imageWidth - var px = Math.round((ax % 1) * (IdentifierTool.tileImageWidth - 1)) - var py = Math.round((ay % 1) * (IdentifierTool.tileImageWidth - 1)) - - IdentifierTool.pxXYs[i] = { x: px, y: py } - - //Tile mouse is over has changed so update the image on our canvas if ( - IdentifierTool.currentTiles[i].x != - IdentifierTool.activeTiles[i].x || - IdentifierTool.currentTiles[i].y != - IdentifierTool.activeTiles[i].y || - IdentifierTool.currentTiles[i].z != - IdentifierTool.activeTiles[i].z + IdentifierTool.activeLayerURLs[i].indexOf( + 'stac-collection:' + ) === 0 ) { - //update active tile - //TODO: Capitalize previous comment - IdentifierTool.activeTiles[i].x = - IdentifierTool.currentTiles[i].x - IdentifierTool.activeTiles[i].y = - IdentifierTool.currentTiles[i].y - IdentifierTool.activeTiles[i].z = - IdentifierTool.currentTiles[i].z - - IdentifierTool.images[i] = new Image() - IdentifierTool.images[i].onload = (function (i) { - return function () { - IdentifierTool.imageData[i] = - IdentifierTool.getImageData( - IdentifierTool.images[i] - ) - } - })(i) - IdentifierTool.images[i].onerror = (function (i) { - return function () { - IdentifierTool.imageData[i] = false - } - })(i) - IdentifierTool.images[i].setAttribute('crossOrigin', '') - - IdentifierTool.images[i].src = ( - IdentifierTool.activeLayerURLs[i] + '' + IdentifierTool.imageData[i] = false + trueValue = true + } else { + var activeZ = + IdentifierTool.activeTiles[i] && + IdentifierTool.activeTiles[i].z + ? IdentifierTool.activeTiles[i].z + : IdentifierTool.zoomLevels[i] + var az = Math.min(activeZ, IdentifierTool.zoomLevels[i]) + var ax = F_.lon2tileUnfloored(lnglatzoom[0], az) + var ay = F_.lat2tileUnfloored(lnglatzoom[1], az) + var tz = az + var tx = Math.floor(ax) + var ty = Math.floor(ay) + //Invert y + if (IdentifierTool.tileFormats[i] == 'tms') + ty = Math.pow(2, tz) - 1 - ty + + IdentifierTool.currentTiles[i] = { x: tx, y: ty, z: tz } + //Default activeTiles if none; + if (!IdentifierTool.activeTiles[i]) + IdentifierTool.activeTiles[i] = { x: 0, y: 0, z: -1 } + + //pixel on canvas. decimal part * imageWidth + var px = Math.round( + (ax % 1) * (IdentifierTool.tileImageWidth - 1) + ) + var py = Math.round( + (ay % 1) * (IdentifierTool.tileImageWidth - 1) ) - .replaceAll('{z}', tz) - .replaceAll('{x}', tx) - .replaceAll('{y}', ty) + + IdentifierTool.pxXYs[i] = { x: px, y: py } + + //Tile mouse is over has changed so update the image on our canvas + if ( + IdentifierTool.currentTiles[i].x != + IdentifierTool.activeTiles[i].x || + IdentifierTool.currentTiles[i].y != + IdentifierTool.activeTiles[i].y || + IdentifierTool.currentTiles[i].z != + IdentifierTool.activeTiles[i].z + ) { + //update active tile + //TODO: Capitalize previous comment + IdentifierTool.activeTiles[i].x = + IdentifierTool.currentTiles[i].x + IdentifierTool.activeTiles[i].y = + IdentifierTool.currentTiles[i].y + IdentifierTool.activeTiles[i].z = + IdentifierTool.currentTiles[i].z + + IdentifierTool.images[i] = new Image() + IdentifierTool.images[i].onload = (function (i) { + return function () { + IdentifierTool.imageData[i] = + IdentifierTool.getImageData( + IdentifierTool.images[i] + ) + } + })(i) + IdentifierTool.images[i].onerror = (function (i) { + return function () { + IdentifierTool.imageData[i] = false + } + })(i) + IdentifierTool.images[i].setAttribute('crossOrigin', '') + + IdentifierTool.images[i].src = ( + IdentifierTool.activeLayerURLs[i] + '' + ) + .replaceAll('{z}', tz) + .replaceAll('{x}', tx) + .replaceAll('{y}', ty) + } } } //Output the data somehow - var htmlInfoString = - "', + ``, null, false, null, @@ -504,10 +558,14 @@ function interfaceWithMMWebGIS() { d3.select('#map').style('cursor', 'crosshair') Map_.map.on('mousemove', IdentifierTool.idPixelMap) + Map_.map.on('mouseout', IdentifierTool.clearCursor) if (L_.hasGlobe) { Globe_.litho .getContainer() .addEventListener('mousemove', IdentifierTool.idPixelGlobe, false) + Globe_.litho + .getContainer() + .addEventListener('mouseout', IdentifierTool.clearCursor, false) //Globe_.shouldRaycastSprites = false Globe_.litho.getContainer().style.cursor = 'crosshair' @@ -516,7 +574,7 @@ function interfaceWithMMWebGIS() { //Share everything. Don't take things that aren't yours. // Put things back where you found them. - var newActive = $('#toolcontroller_sepdiv #' + 'Identifier' + 'Tool') + var newActive = $('#toolcontroller_sepdiv #IdentifierTool') newActive.addClass('active').css({ color: ToolController_.activeColor, }) @@ -527,12 +585,16 @@ function interfaceWithMMWebGIS() { function separateFromMMWebGIS() { CursorInfo.hide() Map_.map.off('mousemove', IdentifierTool.idPixelMap) + Map_.map.off('mouseout', IdentifierTool.clearCursor) //Globe_.shouldRaycastSprites = true if (L_.hasGlobe) { Globe_.litho.getContainer().style.cursor = 'default' Globe_.litho .getContainer() .removeEventListener('mousemove', IdentifierTool.idPixelGlobe) + Globe_.litho + .getContainer() + .removeEventListener('mousemove', IdentifierTool.clearCursor) } if (IdentifierTool.targetId === 'toolContentSeparated_Identifier') { @@ -590,7 +652,35 @@ function bestMatchInLegend(rgba, legendData) { function queryDataValue(url, lng, lat, numBands, layerUUID, callback) { numBands = numBands || 1 var dataPath - if (url.startsWith('/vsicurl/')) { + if (url.startsWith('stac-collection:')) { + fetch( + `${ + mmgisglobal.NODE_ENV === 'development' + ? 'http://localhost:8888' + : '' + }/titilerpgstac/collections/${ + url.split('stac-collection:')[1] + }/point/${lng},${lat}?assets=asset&items_limit=10`, + { + method: 'GET', + headers: { + accept: 'application/json', + }, + } + ) + .then((res) => { + if (res.status === 200) { + return res.json() + } + }) + .then((json) => { + if (json.values) { + if (typeof callback === 'function') callback(json.values) + } + }) + .catch((err) => {}) + return + } else if (url.startsWith('/vsicurl/')) { dataPath = url } else { dataPath = 'Missions/' + L_.mission + '/' + url diff --git a/src/essence/Tools/Layers/LayersTool.css b/src/essence/Tools/Layers/LayersTool.css index 07c2b064..148357d3 100644 --- a/src/essence/Tools/Layers/LayersTool.css +++ b/src/essence/Tools/Layers/LayersTool.css @@ -290,6 +290,59 @@ top: -9px; } +#layersTool .tileCogMin, +#layersTool .tileCogMax { + margin-right: 34px; + z-index: 1; + position: relative; +} + +#layersTool .tileCogMin > div:first-child > div:nth-child(2), +#layersTool .tileCogMax > div:first-child > div:nth-child(2) { + display: flex; +} +#layersTool .tileCogUnits { + background: #222; + padding: 0px 2px 0px 2px; + height: 28px; + margin-top: 1px; + color: #ccc; + font-size: 12px; + border-left: 1px solid #575d60; +} + +#layersTool .tileCogColor { + position: relative; + top: -269px; + margin-bottom: -235px; +} +#layersTool .tileCogColormap { + display: flex; +} +#layersTool .tileCogLegend { + line-height: 28px !important; + display: flex; + justify-content: end; + margin-right: 38px; + font-size: 13px; + color: white; +} +#layersTool .tileCogColormapMap { + width: 100%; + height: 266px; + margin-bottom: -29px; +} +#layersTool #tileCogColormapMapLines { + height: 254px; + margin-left: 280px; + margin-top: -7px; + position: relative; +} +#layersTool #tileCogColormapMapLines li { + width: 28px; + border-top: 2px solid var(--color-a2); + float: right; +} #layersTool .tileCogColormap > div { position: relative; margin-top: 2px; @@ -307,9 +360,11 @@ mix-blend-mode: difference; } #layersTool .tileCogColormap img { - width: 325px; + width: 254px; height: 28px; - margin-left: -12px; + transform: rotateZ(90deg) translateX(125px) translateY(-167px); + border-left: 14px solid #222; + border-right: 14px solid #222; } #layersTool #layersToolList > li.download_on, diff --git a/src/essence/Tools/Layers/LayersTool.js b/src/essence/Tools/Layers/LayersTool.js index be28e56c..ee888222 100644 --- a/src/essence/Tools/Layers/LayersTool.js +++ b/src/essence/Tools/Layers/LayersTool.js @@ -11,6 +11,8 @@ import Filtering from '../../Basics/Layers_/Filtering/Filtering' import Help from '../../Ancillary/Help' import CursorInfo from '../../Ancillary/CursorInfo' +import LegendTool from '../Legend/LegendTool.js' + import tippy from 'tippy.js' import 'markjs' import calls from '../../../pre/calls' @@ -430,24 +432,38 @@ function interfaceWithMMGIS(fromInit) { `
  • `, '
    ', '
    Rescale Min Value
    ', - ``, + '
    ', + ``, + node[i].cogUnits != null ? `
    ${node[i].cogUnits}
    `: '', + '
    ', '
    ', '
  • ', + '
  • -
  • ', + '
  • -
  • ', + '
  • -
  • ', + '
  • -
  • ', + '
  • -
  • ', + '
  • -
  • ', + '
  • -
  • ', `
  • `, '
    ', '
    Rescale Max Value
    ', - ``, + '
    ', + ``, + node[i].cogUnits != null ? `
    ${node[i].cogUnits}
    `: '', + '
    ', '
    ', '
  • ', - `
  • `, - '
    ', - '
    Colormap
    ', - `
    ${node[i].cogColormap}
    `, - ``, - '
    ', - '
  • ' + '
    ', + `
  • `, + `
    `, + ``, + `
      `, + `
      `, + '
    • ', + '
      ' ].join('\n') } // prettier-ignore @@ -889,6 +905,8 @@ function interfaceWithMMGIS(fromInit) { if (!wasOn) Filtering.make($(this).parent().parent(), layerName) } } + + populateCogScale(layerName) }) // Locates/zooms to fill extent of layer @@ -1215,21 +1233,34 @@ function interfaceWithMMGIS(fromInit) { layer = L_.asLayerUUID(layer) layer = L_.layers.data[layer] if (L_.layers.layer[layer.name] === null) return - layer.currentCogMin = parseFloat($(this).val()) + layer.currentCogMin = Math.min( + parseFloat($(this).val()), + layer.currentCogMax == null + ? layer.cogMax || 255 + : layer.currentCogMax + ) + $('.tilerescalecogmin').val(layer.currentCogMin) L_.layers.layer[layer.name].refresh(null, true, { currentCogMin: layer.currentCogMin, }) + populateCogScale(layer.name) }) - $('.tilerescalecogmax').on('change', function () { let layer = $(this).attr('layername') layer = L_.asLayerUUID(layer) layer = L_.layers.data[layer] if (L_.layers.layer[layer.name] === null) return - layer.currentCogMax = parseFloat($(this).val()) + layer.currentCogMax = Math.max( + parseFloat($(this).val()), + layer.currentCogMin == null + ? layer.cogMin || 0 + : layer.currentCogMin + ) + $('.tilerescalecogmax').val(layer.currentCogMax) L_.layers.layer[layer.name].refresh(null, true, { currentCogMax: layer.currentCogMax, }) + populateCogScale(layer.name) }) let tags = [] @@ -1679,6 +1710,59 @@ function interfaceWithMMGIS(fromInit) { ].join('\n') } + function populateCogScale(layerName) { + let layer = L_.asLayerUUID(layerName) + layer = L_.layers.data[layer] + if (L_.layers.layer[layer.name] === null) return + if (!layer.url.startsWith('stac-collection:')) return + + const dynamicLegendConf = [] + const imgElement = document.getElementById('titlerCogColormapImage') + const canvasElement = document.createElement('canvas') + document.body.appendChild(canvasElement) + canvasElement.style.display = 'none' + canvasElement.width = 256 + canvasElement.height = 1 + const context = canvasElement.getContext('2d') + context.drawImage(imgElement, 0, 0, 256, 1, 0, 0, 256, 1) + + const min = + layer.currentCogMin == null ? layer.cogMin : layer.currentCogMin + const max = + layer.currentCogMax == null ? layer.cogMax : layer.currentCogMax + for (let i = 0; i < 9; i++) { + let label = `${ + Math.round(F_.linearScale([0, 8], [min, max], i) * 100) / 100 + }${layer.cogUnits || ''}` + if (i !== 0 && i !== 8) { + $(`#tileCogLegend_${i}`).text(label) + } + const c = context.getImageData( + parseInt((255 / 9) * i), + 0, + 1, + 1 + ).data + dynamicLegendConf.push({ + color: `rgb(${c[0]}, ${c[1]}, ${c[2]})`, + strokecolor: null, + shape: 'continuous', + value: label, + }) + } + document.body.removeChild(canvasElement) + + L_.layers.data[layer.name]._legend = dynamicLegendConf + LegendTool.refreshLegends() + + $('#tileCogColormapMapLines').empty() + for (let i = 0; i < 9; i++) { + $('#tileCogColormapMapLines').append( + `
    • ` + ) + } + } + function setSublayerEvents() { //Applies slider values to map layers $('.transparencyslider').off('input') diff --git a/src/essence/Tools/Legend/LegendTool.js b/src/essence/Tools/Legend/LegendTool.js index 59dc5513..1466ee82 100644 --- a/src/essence/Tools/Legend/LegendTool.js +++ b/src/essence/Tools/Legend/LegendTool.js @@ -64,6 +64,7 @@ var LegendTool = { L_.unsubscribeOnLayerToggle('LegendTool') this.made = false }, + refreshLegends: refreshLegends, overwriteLegends: overwriteLegends, } @@ -74,20 +75,35 @@ function interfaceWithMMWebGIS() { } separateFromMMWebGIS() - let tools = drawLegendHeader() + LegendTool.tools = drawLegendHeader() //Add the markup to tools or do it manually //tools.html( markup ); //Add event functions and whatnot //Draw legends - var first = true + LegendTool.refreshLegends() + //Share everything. Don't take things that aren't yours. + // Put things back where you found them. + function separateFromMMWebGIS() { + let tools = d3.select( + LegendTool.targetId ? `#${LegendTool.targetId}` : '#toolPanel' + ) + tools.style('background', 'var(--color-k)') + //Clear it + tools.selectAll('*').remove() + } +} + +function refreshLegends() { + $('#LegendTool').empty() + for (let l in L_.layers.on) { if (L_.layers.on[l] == true) { if (L_.layers.data[l].type != 'header') { if (L_.layers.data[l]?._legend != undefined) { drawLegends( - tools, + LegendTool.tools, L_.layers.data[l]?._legend, l, L_.layers.data[l].display_name, @@ -97,17 +113,6 @@ function interfaceWithMMWebGIS() { } } } - - //Share everything. Don't take things that aren't yours. - // Put things back where you found them. - function separateFromMMWebGIS() { - let tools = d3.select( - LegendTool.targetId ? `#${LegendTool.targetId}` : '#toolPanel' - ) - tools.style('background', 'var(--color-k)') - //Clear it - tools.selectAll('*').remove() - } } // The legends parameter should be an array of objects, where each object must contain @@ -151,7 +156,10 @@ function drawLegendHeader() { .style('height', '30px') .style('line-height', '30px') .style('font-size', '13px') - .style('padding-right', '8px') + .style( + 'padding-right', + LegendTool.justification === 'right' ? '30px' : '8px' + ) .style( 'padding-left', LegendTool.justification === 'right' ? '10px' : '30px'