Skip to content

Conversation

@tealefristoe
Copy link
Contributor

@tealefristoe tealefristoe commented Mar 28, 2025

Jira story: https://concord-consortium.atlassian.net/browse/CODAP-335

This PR Is about displaying geotiff files over maps in Codap.

The first step was introducing a geotiffUrl field to MapContentModel. When this field is set, the map will download and display the geotiff at the specified url.

Currently, there is no UI in Codap to set this field. The only way to set it is to make an update map API request. Unfortunately, the map API handlers were very bare bones before this PR, so I had to expand them quite a bit to even get this working.

The next step is actually displaying the geotiff in leaflet, which has proven to be quite challenging. I was able to find two libraries that accomplish this. I also experimented with displaying a PNG or JPEG map in leaflet. Unfortunately, none of these methods worked particularly well. Code has been left in map-interior.tsx to easily try any of the three options--just comment out the two you don't want to use.

Testing

  1. Load a blank document in Codap running this branch.
  2. Add a map to the document.
  3. Add the API tester plugin to the document: https://concord-consortium.github.io/codap-data-interactives/DataInteractiveAPITester/index.html?lang=en
  4. Send a request like this to Codap:
{
  "action": "update",
  "resource": "component[Map]",
  "values": {
    "geotiffUrl": "https://neo.gsfc.nasa.gov/servlet/RenderData?si=1990677&cs=rgb&format=TIFF&width=360&height=180"
  }
}
  • You can try other geotiffs by replacing the geotiffUrl value. See below for instructions.
  1. If the request fails, check the javascript console for a clue why.

Geotiff Data
Data can be found here: https://neo.gsfc.nasa.gov/dataset_index.php#atmosphere
Datasets of interest are:

To get the url for a particular dataset:

  1. Visit the dataset of interest.
  2. Choose the year and month of interest.
  3. Make sure the File Type is GeoTIFF (raster).
  4. Right click on the resolution of interest and select Copy Link Address.
  5. Paste the url in the above API request, specifying geotiffUrl.

georaster-layer-for-leaflet

  • Pro: Popular library with ~4000 weekly downloads and last published a year ago.
  • Pro: Automatically colors maps properly.
  • Pro: Covers repeated world map.
  • Pro: Works for more datasets.
  • Con: Seems to render lower resolution than possible.

Results:

  • Rainfall
    • 1 degree: success
    • .5 degrees: Invalid array length
    • .25 degrees: success
    • .1 degrees: Invalid byte order
  • Carbon Monoxide
    • 1 degree: Invalid byte order
  • Nitrogen Dioxide
    • 1 degree: Invalid byte order
    • .5 degrees: Invalid byte order
    • .25 degrees: Invalid byte order
  • Vegetation Index
    • 1 degree: Invalid byte order
    • .5 degrees: Invalid byte order
    • .25 degrees: Invalid byte order
    • .1 degrees: Invalid byte order
  • Land Surface Temperature
    • 1 degree: Invalid byte order
    • .5 degrees: Invalid byte order
    • .25 degrees: Invalid byte order
    • .1 degrees: Invalid byte order
  • Active Fires
    • 1 degree: Invalid byte order
    • .5 degrees: Invalid byte order
    • .25 degrees: Invalid byte order
    • .1 degrees: Invalid byte order

Errors:
Invalid array length
This error occurs while running parseGeoraster from the georaster library.

7a6eaeef-9c63-44f2-8b44-0933b95de71c:10 Uncaught (in promise) RangeError: Invalid array length
    at Array.push (<anonymous>)
    at c (7a6eaeef-9c63-44f2-8b44-0933b95de71c:10:104204)
    at C (7a6eaeef-9c63-44f2-8b44-0933b95de71c:10:104446)
    at O.decodeBlock (7a6eaeef-9c63-44f2-8b44-0933b95de71c:10:104741)
    at O.decode (7a6eaeef-9c63-44f2-8b44-0933b95de71c:10:102515)
    at V.getTileOrStrip (7a6eaeef-9c63-44f2-8b44-0933b95de71c:10:119089)

Invalid byte order
This error occurs while running parseGeoraster from the georaster library.

Uncaught (in promise) TypeError: Invalid byte order value.
    at Tt.fromSource (f96a461d-c172-4c0b-adc8-fd96dc60b385:10:142141)

Failed to fetch
This error occurs irregularly (once while trying Land Surface Temperature .1 degrees during recent testing). It happens while running response.arrayBuffer() in getGeoraster in geotiff-utils.ts.

geotiff-utils.ts:11 
            
            
           GET https://neo.gsfc.nasa.gov/servlet/RenderData?si=1850864&cs=rgb&format=JPEG&width=3600&height=1800 net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)
eval @ geotiff-utils.ts:11
step @ geotiff-utils.ts:1
eval @ geotiff-utils.ts:1
asyncGeneratorStep @ geotiff-utils.ts:1
_next @ geotiff-utils.ts:1
eval @ geotiff-utils.ts:1
eval @ geotiff-utils.ts:1
getGeoraster @ geotiff-utils.ts:9
eval @ geotiff-utils.ts:30
step @ geotiff-utils.ts:1
eval @ geotiff-utils.ts:1
asyncGeneratorStep @ geotiff-utils.ts:1
_next @ geotiff-utils.ts:1
eval @ geotiff-utils.ts:1
eval @ geotiff-utils.ts:1
createGeoTIFFLayerWithGeorasterLayerForLeaflet @ geotiff-utils.ts:28
addGeoTIFFLayer @ map-interior.tsx:50
commitHookEffectListMount @ react-dom.development.js:23184
commitPassiveMountOnFiber @ react-dom.development.js:24965
commitPassiveMountEffects_complete @ react-dom.development.js:24925
commitPassiveMountEffects_begin @ react-dom.development.js:24912
commitPassiveMountEffects @ react-dom.development.js:24900
flushPassiveEffectsImpl @ react-dom.development.js:27073
flushPassiveEffects @ react-dom.development.js:27018
commitRootImpl @ react-dom.development.js:26969
commitRoot @ react-dom.development.js:26716
performSyncWorkOnRoot @ react-dom.development.js:26151
flushSyncCallbacks @ react-dom.development.js:12042
eval @ react-dom.development.js:25685Understand this errorAI
geotiff-utils.ts:19 Error fetching and processing geoTIFF TypeError: Failed to fetch

leaflet-geotiff-2

  • Pro: Renders at higher resolution.
  • Con: Less popular library, just ~360 weekly downloads and last published three years ago.
  • Con: Requires custom renderer to color the map properly.
    • Seems to require downloading the geotiff twice twice, once to access the color map, once to actually render it. We could store the colormaps ourselves so they don't need to be downloaded all the time, but this still complicates things and adds a possible point of failure.
  • Con: Only renders the map a single time, even when the world map is repeated.
  • Con: Only works for a handful of datasets.

Note: An earlier test version used the geotiff library to access the geotiff's color map, but I'm now using georaster instead. That means that the color version of this library no longer works with dataset/resolution combos that fail due to georaster.

While doing this thorough test, I was surprised to see that things worked differently than they did previously. My best guess why is the geotiff -> georaster change (even though I turned off color for these tests). In any case, Active Fires used to work for leaflet-geotiff-2, but no longer.

Results:

  • Rainfall
    • 1 degree: success
    • .5 degrees: Invalid array length
    • .25 degrees: Invalid typed array length
    • .1 degrees: Invalid typed array length
  • Carbon Monoxide
    • 1 degree: this.tiff.getImage is not a function
  • Nitrogen Dioxide
    • 1 degree: this.tiff.getImage is not a function
    • .5 degrees: this.tiff.getImage is not a function
    • .25 degrees: this.tiff.getImage is not a function
  • Vegetation Index
    • 1 degree: this.tiff.getImage is not a function
    • .5 degrees: this.tiff.getImage is not a function
    • .25 degrees: this.tiff.getImage is not a function
    • .1 degrees: this.tiff.getImage is not a function
  • Land Surface Temperature
    • 1 degree: this.tiff.getImage is not a function
    • .5 degrees: this.tiff.getImage is not a function
    • .25 degrees: this.tiff.getImage is not a function
    • .1 degrees: this.tiff.getImage is not a function
  • Active Fires
    • 1 degree: this.tiff.getImage is not a function
    • .5 degrees: this.tiff.getImage is not a function
    • .25 degrees: this.tiff.getImage is not a function
    • .1 degrees: this.tiff.getImage is not a function

Errors:
Invalid typed array length
This error occurs when calling L.leafletGeotiff.

image.readRasters threw error RangeError: Invalid typed array length: -2102
    at new Uint8Array (<anonymous>)
    at y (geotiff.js:339:2551)
    at e.eval (geotiff.js:339:5083)
    at h (geotiff.js:2:966)
    at Generator.eval [as _invoke] (geotiff.js:2:754)
    at t.<computed> [as next] (geotiff.js:2:1388)
    at n (geotiff.js:20:41)
    at c (geotiff.js:20:243)

Invalid array length

image.readRasters threw error RangeError: Invalid array length
    at Array.push (<anonymous>)
    at b (geotiff.js:136:1658)
    at v (geotiff.js:136:1887)
    at o.value (geotiff.js:136:2352)
    at o.value (geotiff.js:132:398)
    at u.eval (geotiff.js:172:4137)
    at h (geotiff.js:2:966)
    at Generator.eval [as _invoke] (geotiff.js:2:754)
    at t.<computed> [as next] (geotiff.js:2:1388)
    at n (geotiff.js:20:41)

this.tiff.getImage is not a function
When this error occurs, the geotiff is rendered over the map, but it is improperly placed and projected. The rendered geotiff is even in color when the ColorMapRenderer is set to render in black and white.

this.tiff.getImage is not a function
TypeError: this.tiff.getImage is not a function
    at NewClass._processTIFF (webpack-internal:///./node_modules/leaflet-geotiff-2/dist/leaflet-geotiff.js:187:39)

This seems to be the result of the same error seen in georaster-layer-for-leaflet:

geotiff-utils.ts:132 Failed to load from url or blob https://neo.gsfc.nasa.gov/servlet/RenderData?si=1990616&cs=rgb&format=JPEG&width=3600&height=1800 TypeError: Invalid byte order value.
    at eval (geotiff.js:347:13237)
    at h (geotiff.js:2:966)
    at Generator.eval [as _invoke] (geotiff.js:2:754)
    at t.<computed> [as next] (geotiff.js:2:1388)
    at n (geotiff.js:20:41)
    at c (geotiff.js:20:243)

PNG and JPEG
These are easy to display using basic leaflet. Unfortunately, without the projection provided with geotiff renders, these maps do not fit the Codap maps properly.

@tealefristoe tealefristoe marked this pull request as draft March 28, 2025 02:04
@codecov
Copy link

codecov bot commented Mar 28, 2025

Codecov Report

Attention: Patch coverage is 24.86772% with 142 lines in your changes missing coverage. Please review.

Project coverage is 69.86%. Comparing base (4100e6c) to head (a31c1fd).
Report is 79 commits behind head on main.

Files with missing lines Patch % Lines
v3/src/components/map/utilities/geotiff-utils.ts 6.66% 112 Missing ⚠️
v3/src/components/map/map-component-handler.ts 70.45% 13 Missing ⚠️
v3/src/components/map/components/map-interior.tsx 21.42% 11 Missing ⚠️
v3/src/components/map/models/map-content-model.ts 0.00% 6 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (4100e6c) and HEAD (a31c1fd). Click for more details.

HEAD has 9 uploads less than BASE
Flag BASE (4100e6c) HEAD (a31c1fd)
cypress 10 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #1885       +/-   ##
===========================================
- Coverage   86.36%   69.86%   -16.51%     
===========================================
  Files         673      675        +2     
  Lines       34737    34887      +150     
  Branches     9841     9864       +23     
===========================================
- Hits        30000    24373     -5627     
- Misses       4579    10356     +5777     
  Partials      158      158               
Flag Coverage Δ
cypress 42.57% <2.92%> (-30.27%) ⬇️
jest 56.14% <23.80%> (-0.21%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress
Copy link

cypress bot commented Mar 28, 2025

codap-v3    Run #7006

Run Properties:  status check passed Passed #7006  •  git commit a31c1fd937: handle .png files
Project codap-v3
Branch Review codap-335-geotiff
Run status status check passed Passed #7006
Run duration 01m 44s
Commit git commit a31c1fd937: handle .png files
Committer Scott Cytacki
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 3
View all changes introduced in this branch ↗︎

@scytacki
Copy link
Member

scytacki commented Mar 31, 2025

I took a look at this more: the PR, the libraries, Leaflet, the NEO files, and the projection standards. Now it makes sense that we can't just use the png images directly in Leaflet.

@tealefristoe you might have figured this all out already, but I'll summarize here for the record.

Leaflet always works in the spherical mercator projection where the image get stretched both vertically and horizontally as you move towards the poles so angles are preserved and the aspect ratio is more maintained when zoomed in. This projection is defined with the code EPSG:3857

The NEO images are in basic lat,lon projection. So this vertically squishes the image as you move towards the poles. This projection seems to be the most common one used by GeoTiff. This projection is defined with the code EPSG:4326. The library leaflet-geotiff-2 seems to only support converting from the lat,lon projection to the spherical mercator projection. While georaster-layer-for-leaflet supports additional projections used by the GeoTIFF file.

Additionally the transform of the GeoTIFF raster to the leaftlet projection can't be done via simple html5 canvas or CSS transforms. So my idea that the NEO pngs would be better because we could render them directly onto a cavas without reading through the pixels ourselves is not possible. I suspect there are some WebGL shader based transforms which can do this kind of conversion very efficiently. There was hint at this in the docs of one of the leaflet libraries.

Notes on the libraries:

  • both libraries use geotiff.js under the hood. But georaster-layer-for-leaflet has this georaster layer in between.
  • both libraries at least have an option to do the decompression in a webworker so that it doesn't block the browser rendering thread. At least one of them supports multiple webworkers to speed it up.
  • the georaster library can also read pngs
  • in the leaflet-geotiff-2 library the projection code is here: https://github.com/onaci/leaflet-geotiff-2/blob/master/src/leaflet-geotiff.js#L512 It isn't too complex, so if we need to do this ourselves for some reason that is a good reference. It has to take into account the projection, the transformation (normally just scaling and translation) in the GeoTIFF, as well as transformation in Leaflet, and finally the zoom level in leaflet.
  • I haven't tracked down where all of that same code is in georaster-layer-for-leaflet. I know it uses proj4-fully-loaded which is a library for doing projections.

So given all of this. I think it would be best for me to spend a bit more time on georaster-layer-for-leaflet approach. To see if I can quickly track down the bugs. Then we'd have a working solution which we could try out. The leaflet-geotiff-2 library is more simple so it ought to be easier to fix bugs in, but since the georaster-layer-for-leaflet was closer to working and has more recent commits it seems more worthwhile to look at further.

My concern as before is that this approach will be too slow when we are trying to animate 250 images on the map. For each frame the code will have to decode the image, and project it. So if this proves to be a problem, then I think we'll need to "pre-project" the images and store those on the server. This way all the animation is doing is loading in the images using the browsers built in native decompression. But even though this is a concern it still seems best to put a little more time into the slower options because I'm hopeful there will be a small bug to fix and we can have something for people to play with sooner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v3 CODAP v3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants