Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a49a96
CODAP-335 add georaster support
scytacki Apr 22, 2025
f6525fe
CODAP-335 re-use existing layer when the image is updated
scytacki Apr 24, 2025
6da8ad8
Merge branch 'main' into CODAP-335-georaster
scytacki Apr 29, 2025
a15f93a
working version of internal georaster-layer
scytacki Apr 29, 2025
c211b52
fix flicker when updating georaster
scytacki Apr 29, 2025
035173b
fix lint errors
scytacki Apr 29, 2025
7469d56
improved geo raster layer types
scytacki Apr 30, 2025
e98aaea
use typescript class to improve typing
scytacki Apr 30, 2025
abef646
simplified georaster-layer
scytacki May 5, 2025
43c8b1f
make internal GeoExtent
scytacki May 5, 2025
81e7a00
add unit tests of the geo-extent
scytacki May 6, 2025
dba1eb6
remove unused properties
scytacki May 7, 2025
c60edf3
remove unnecessary sourceType
scytacki May 7, 2025
597a139
remove more unused georaster properties
scytacki May 7, 2025
4b1d193
simplify geo raster layer options
scytacki May 7, 2025
b586846
require georaster to have values
scytacki May 7, 2025
515be05
simplify to just a single georaster in the layer
scytacki May 7, 2025
8660772
move georaster update into the layer
scytacki May 7, 2025
cf899be
remove use of 'rasters' property from layer
scytacki May 7, 2025
68d9a5a
use html image and canvas to load the raster
scytacki May 7, 2025
b0e0ea0
update documentation and license
scytacki May 7, 2025
c73d4bb
add a note about the geo-extent license
scytacki May 7, 2025
826260d
self code review
scytacki May 7, 2025
a2f1156
add basic geo raster cypress test
scytacki May 9, 2025
5770772
add a test which updates an existing geo raster
scytacki May 9, 2025
ada8a27
address PR comments
scytacki May 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion v3/cypress/e2e/map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ToolbarElements as toolbar } from "../support/elements/toolbar-elements
import { CfmElements as cfm } from "../support/elements/cfm"
import { MapLegendHelper as mlh } from "../support/helpers/map-legend-helper"
import { TableTileElements as table } from "../support/elements/table-tile"
import { WebViewTileElements as webView } from "../support/elements/web-view-tile"

const filename1 = "cypress/fixtures/RollerCoastersWithLatLong.csv"
const filename2 = "cypress/fixtures/map-data.csv"
Expand Down Expand Up @@ -396,7 +397,6 @@ context("Map UI", () => {
mlh.selectCategoryColorForCategoricalLegend(arrayOfValues[0].values[1])
mlh.verifyCategoricalLegendKeySelected(arrayOfValues[0].values[1], arrayOfValues[0].selected[1])
})

it("shows connecting lines when Connecting Lines option is checked", () => {
cfm.openLocalDoc(filename1)
c.getIconFromToolShelf("map").click()
Expand All @@ -412,5 +412,77 @@ context("Map UI", () => {
cy.wait(2000)
cy.get("[data-testid=connecting-lines-map-1").find("path").should("have.length", 0)
})
})

context("Map API", () => {
beforeEach(function () {
const url = `${Cypress.config("index")}#file=examples:Four%20Seals`
cy.visit(url)
})
const apiTesterUrl='https://concord-consortium.github.io/codap-data-interactives/DataInteractiveAPITester/index.html?lang=en'
const openAPITester = () => {
toolbar.getOptionsButton().click()
toolbar.getWebViewButton().click()
webView.enterUrl(apiTesterUrl)
cy.wait(1000)
}
it("supports a background georaster", () => {
openAPITester()

// Make sure the API tester is loaded
webView.getTitle().should("contain.text", "CODAP API Tester")

// Check that the geo raster layer is not there
map.getMapGeoRasterLayer().should("not.exist")

cy.log("Handle initial setting of the geoRaster")
const cmd1 = `{
"action": "update",
"resource": "component[Measurements]",
"values": {
"geoRaster": {
"type": "png",
"url": "https://models-resources.concord.org/neo-images/v1/GPM_3IMERGM/720x360/2007-09-01.png"
}
}
}`
webView.sendAPITesterCommand(cmd1)
webView.confirmAPITesterResponseContains(/"success":\s*true/)
webView.clearAPITesterResponses()

map.getMapGeoRasterLayer().should("exist")

cy.log("Handle updating the geoRaster")
const cmd2 = `{
"action": "update",
"resource": "component[Measurements]",
"values": {
"geoRaster": {
"type": "png",
"url": "https://models-resources.concord.org/neo-images/v1/GPM_3IMERGM/720x360/2007-10-01.png"
}
}
}`
webView.sendAPITesterCommand(cmd2)
webView.confirmAPITesterResponseContains(/"success":\s*true/)
webView.clearAPITesterResponses()

// TODO: we should check that the image updated somehow, I think this would require reading a pixel
// of the canvas and checking that it changed.
map.getMapGeoRasterLayer().should("exist")

cy.log("Remove the geoRaster")
const cmd3 = `{
"action": "update",
"resource": "component[Measurements]",
"values": {
"geoRaster": null
}
}`
webView.sendAPITesterCommand(cmd3)
webView.confirmAPITesterResponseContains(/"success":\s*true/)
webView.clearAPITesterResponses()

map.getMapGeoRasterLayer().should("not.exist")
})
})
3 changes: 3 additions & 0 deletions v3/cypress/support/elements/map-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const MapTileElements = {
getMapPin(index=0) {
return this.getMapPins().eq(index)
},
getMapGeoRasterLayer() {
return this.getMapTile().find(".leaflet-overlay-pane .leaflet-layer")
},
getHideSelectedCases() {
return c.getInspectorPanel().find("[data-testid=hide-selected-cases]")
},
Expand Down
136 changes: 136 additions & 0 deletions v3/doc/georasters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Geo Rasters
CODAP supports adding a layer to the map which displays an image projected onto the map. Currently this image must be a PNG. It is only supported via the CODAP plugin API. You can test it by:
1. Add the Plugin API Tester to a CODAP document:
https://concord-consortium.github.io/codap-data-interactives/DataInteractiveAPITester/index.html?lang=en
2. Add a map to the CODAP document
3. Send a message to CODAP like the examples below.

## JSON format

```json
"geoRaster": {
"url": "url_to_image",
"type": "png",
"opacity": 0.5,
}
```

The `opacity` is optional and defaults to `0.5`.

The image is assumed to span the whole globe from -90 to 90 latitude and -180 to 180 longitude. The image is also assumed to be in an EPSG 4326 "projection".

## Example CODAP messages

### Rainfall

0.5 deg resolution:
```json
{
"action": "update",
"resource": "component[Map]",
"values": {
"geoRaster": {
"type": "png",
"url": "https://models-resources.concord.org/neo-images/v1/GPM_3IMERGM/720x360/2007-09-01.png"
}
}
}
```

0.25 Resolution
```json
{
"action": "update",
"resource": "component[Map]",
"values": {
"geoRaster": {
"type": "png",
"url": "https://neo.gsfc.nasa.gov/servlet/RenderData?si=1990677&cs=rgb&format=PNG&width=1440&height=720"
}
}
}
```

0.1 Resolution
```json
{
"action": "update",
"resource": "component[Map]",
"values": {
"geoRaster": {
"type": "png",
"url": "https://neo.gsfc.nasa.gov/servlet/RenderData?si=1990677&cs=rgb&format=PNG&width=3600&height=1800"
}
}
}
```

### Other datasets
You can try other datasets by:
1. Find a dataset here: https://neo.gsfc.nasa.gov/dataset_index.php
2. Choose the year and month of interest.
3. Make sure the File Type is PNG.
4. Right click on the resolution of interest and select Copy Link Address.
5. Replace the `geoRaster.url` in one of the messages above.

## What about GeoTIFFs
We initially tried to support real GeoTIFFs. They include information about the position of the raster in the world, and what map projection the raster is using. So they would be a more self contained way of sending map rasters to CODAP. However the libraries we found to support them seem to be buggy. Additionally the the size of the GeoTIFFs is a bit larger than PNGs.

All of the Leaflet libraries we found use the [geotiff.js](https://github.com/geotiffjs/geotiff.js) library underneath.

This library is popular, but it is difficult to get a handle on how it is maintained. There are a few open PRs that seem reasonable but don't have any responses from the maintainer. Meanwhile there are several PRs that have been merged recently with very little discussion. It looks like the maintainer trusts certain contributors or there is another channel where communication about the PRs is happening.

We found that it crashes when trying to load some images from https://neo.gsfc.nasa.gov. For example: https://neo.gsfc.nasa.gov/servlet/RenderData?si=1990677&cs=rgb&format=TIFF&width=720&height=360 This file will cause node.js to crash with a native stack trace. When geotiff.js is used in the browser the error message about this file is more useful. If there is time and support it would probably be worthwhile for us to track down the issue and fix geotiff.js so CODAP can support GeoTIFFs.

Besides this error we've seen errors just downloading the GeoTIFFs.

## Projections

EPSG 4326 is the "projection" where each x,y coordinate represents a lat,long coordinate. It might be in different scales so a pixel might be one degree, 0.5 degrees, ...

EPSG 3857 is the "google maps" projection where things are stretched vertically as you go more north or south from the equator. The stretching is done so the distance in meters at a point stays the same both vertically and horizontally.

## Performance

Originally we used the georaster-layer-for-leaflet and png-codec packages to implement this. These had some performance issues which were fixed by bringing this code into CODAP and significantly modifying it.

The speed of the rendering will vary depending on the zoom level of the map. When zoomed in the rendering inner loop has fewer raster pixels to iterate over.

### Notes on georaster-layer-for-leaflet package with png-codec

The creation of the GeoRaster for each 0.5 degree per pixel image took around 40ms. This was mostly taken up by the png fetching from cache (11ms), the png decoding (20ms), converting the decoded pixels to the GeoRaster format (8ms).

Then this GeoRaster was either used to create a new GeoRasterLayer or update an existing GeoRasterLayer. Each tile location in Leaflet asks the GeoRasterLayer to create a tile for this location and render this tile. On the full map view there are 3 tiles which are really the same image repeated 3 times. The total time for all of this rendering was around 110ms.

These times were calculated using the Chrome dev tools profile tool along with some `performance.measure(...)` calls in the code.

Another way to profile was by looking at what speed the images can be cycled through and still give consistent updates:
- For the default map (full world view with repeats), we could update at a rate of 6 images a second before the updates started to fall behind. This matched up with the 150ms total time described above.
- For a map zoomed into just North America, it could go about 15 frames a second. So that was around 67 ms rendering time for each image.

Note that in the North America view the creation of the GeoRaster should have taken the same amount of time as with the full world view. That means that the 40ms to create the GeoRaster really dominated the total time on the North America view.

#### Flicker
Originally we were creating a new GeoRasterLayer on each image update. This layer was not removed until the new GeoRaster was successfully created. However this still resulted in a flicker because the layer's tiles would be hidden instantly and the drawing of the new tiles would happen async over 20ms to 110ms. So this resulted in a pretty bad flicker.

Now the GeoRasterLayer is updated if it can be instead of making a new layer. This eliminated one reason for the flicker, but then exposed another issue. In the georaster-layer-for-leaflet package the `GeoRasterLayer.drawTile` method sets the width and height of the canvas of the tile even if width and height don't change. As documented by MDN, setting either of these properties automatically clears the canvas even if the values are the same.

This was fixed in the `georaster-layer-leaflet.ts` module that is part of CODAP.

### Canceling
When the geoRaster is updated too quickly, the fetching and decoding of the geoRaster might not complete before the next updated. Additionally within the `GeoRasterLayer.drawTile` the actual rendering happens async after the initial canvas configuration, so this might also not have completed. The code currently bails out of the fetching and decoding if the geoRaster URL has changed during the fetch or decoding.

This could be further improved by bailing out of `GeoRasterLayer.drawTile` if the url has changed.

### Notes
- the "resolution" of the GeoRasterLayer can be changed. This ought to speed up its rendering, but the "raster pixel" edges shown when zoomed in won't match up as well with the actual "raster pixels".
- for the absolute fastest rendering we would need to pre-render the projected geo-raster so the map is just displaying a tile source like any other tile source. However this would require a lot of storage and more network bandwidth as the user is zooming in and out. So in the end it might not be worth it.

# Javascript Size
When this feature was being developed the size of main CODAP javascript bundled file was 6.6MB before the feature was added.

Initially the georaster-layer-for-leaflet package was used along with png-codec to load in the png image. This approach caused the main javascript file to go up to 7.2MB. So about a 600KB increase.

The georaster-layer-for-leaflet code was brought into CODAP directly and stripped down so it no longer supports lots of different types of projections which required the Proj4j package. Additionally the png-codec package was replaced with loading the png into a canvas. These changes improved the speed of the geo raster support. They also brought the size of the main javascript down to 6.7MB. So about a 100KB increase.

The extra size could be reduced further by looking at the use of snap package by georaster-layer-for-leaflet. This snap package uses the preciso, regenerator-runtime, and snap-bb all of which are probably unnecessary and add around 24KB.
61 changes: 55 additions & 6 deletions v3/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions v3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"@lezer/lr": "^1.4.2",
"@nedb/binary-search-tree": "^2.1.5",
"@uiw/react-codemirror": "^4.23.10",
"bbox-fns": "^0.20.2",
"clsx": "^2.1.1",
"colord": "^2.9.3",
"create-react-class": "^15.7.0",
Expand Down Expand Up @@ -238,8 +239,10 @@
"react-dom-factories": "^1.0.2",
"react-leaflet": "^4.2.1",
"react-resize-detector": "^12.0.2",
"regenerator-runtime": "^0.14.1",
"rtree": "^1.4.2",
"simpleheat": "^0.4.0",
"snap-bbox": "^0.5.0",
"tslib": "^2.8.1",
"type-fest": "^4.37.0",
"use-debounce": "^10.0.4",
Expand Down
Loading