Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle rotated, skewed or flipped GeoTIFF tile grids #15402

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

mike-000
Copy link
Contributor

@mike-000 mike-000 commented Dec 5, 2023

Fixes #15294
Fixes #15370
Fixes #15428

Uses rotated/skewed/flipped variants of projections to handle GeoTIFF tile grids with rotation, skew or flip specified by ModelTransformation.

See https://deploy-preview-15402--ol-site.netlify.app/en/latest/examples/cog-modeltransformation.html

Uses a local alternative to geotiffjs/geotiff.js#403 which could be removed if the replacement geotiffjs/geotiff.js#413 (with corrected Y axis scale) is merged. The code could be further simplified if geotiffjs/geotiff.js#414 is merged.

Copy link

github-actions bot commented Dec 5, 2023

📦 Preview the website for this branch here: https://deploy-preview-15402--ol-site.netlify.app/.

@mike-000
Copy link
Contributor Author

mike-000 commented Dec 5, 2023

Ready for review as I do not have any files small enough to use for a rotated or skewed rendering test, although the test file from #15428 would be suitable. The Projection setMatrix method should work equally well for a rotated ImageStatic, etc. but I have not documented that as part of the API.

Copy link

📦 Preview the website for this branch here: https://deploy-preview-15402--ol-site.netlify.app/.

@mike-000 mike-000 changed the title Handle rotated or skewed GeoTIFF tile grids Handle rotated, skewed or flipped GeoTIFF tile grids Dec 16, 2023
@loganwilliams
Copy link

@mike-000 Thanks for this! I'm happy to provide more test files, though the smallest I have is ~80MB.

@mike-000
Copy link
Contributor Author

@loganwilliams There is a large selection of open data available via https://radiantearth.github.io/stac-browser/#/external/s3.us-west-2.amazonaws.com/umbra-open-data-catalog/stac/catalog.json typically around that size suitable for use in examples but too big to use as local test data. I have now found a reduced version of the file used in the example at https://github.com/GeoTIFF/test-data which is under 4MB so might be suitable for another rendering test.

@loganwilliams
Copy link

This is really great, thanks for your work on it @mike-000. Hope it gets merged soon.

Copy link
Member

@ahocevar ahocevar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach developed in this pull request is good. My suggestions are only about where parts of the implementation belong to, so more users can benefit from it.

Comment on lines 128 to 134
/**
* Replacement for GeoTIFFImage.getResolution() to handle ModelTransformation.
* @param {GeoTIFFImage} image The image.
* @param {GeoTIFFImage} [referenceImage] The reference image.
* @return {Array<number>} The map x and y units per pixel.
*/
function getResolution(image, referenceImage) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to contribute this to geotiff.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, if geotiffjs/geotiff.js#413 and geotiffjs/geotiff.js#414 are accepted and included in a release then these copies could be replaced by the real thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both are now merged and released.

src/ol/proj.js Outdated
Comment on lines 486 to 491
if (sourceMatrix || destinationMatrix) {
const transform = transformFunc;
sourceMatrix = invertMatrix((sourceMatrix || createMatrix()).slice());
destinationMatrix = destinationMatrix || createMatrix();
const coordTransform = function (coordinate) {
return applyMatrix(
destinationMatrix,
transform(applyMatrix(sourceMatrix, coordinate.slice())),
);
};
transformFunc = createTransformFromCoordinateTransform(coordTransform);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't bake matrix transforms into projections. It does not feel right. Instead, a matrix transform should be defined with custom transform functions using addCoordinateTransforms(), like e.g. shown in https://openlayers.org/en/latest/examples/wms-custom-proj.html.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with that approach is transforms would need to be maintained between every matrix variation and every projection added (past and future) via proj4. Given that this is a relatively unusual situation (although it could also be applied to KML ground overlays) I think setting an additional property on the projection and calculating the transforms dynamically as required would be easier.

Copy link
Member

@ahocevar ahocevar Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you create a transform function, you can mix in the matrix transform from a separate function or variable, so you can still handle matrix variations outside the once created transform function. Basically the same thing you're currently doing in src/ol/proj.js.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing transform functions are indexed by projection codes - the matrixes are assigned to projection objects with those codes. Doing it this way avoids adding complexity to the indexing of the primary transforms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you point me to the code section where that happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new code calls projection.setMatrix(matrix); or this.projection.setMatrix(matrix); when determining the projection https://github.com/mike-000/openlayers/blob/7cc890b34747feb99173b974d2e6e143c88e0504/src/ol/source/GeoTIFF.js#L297
and if a matrix is detected when an override projection has been specified
https://github.com/mike-000/openlayers/blob/7cc890b34747feb99173b974d2e6e143c88e0504/src/ol/source/GeoTIFF.js#615

It is handled in existing reprojection code via getTransform and transform calls which need no changes as they are passed the projection objects (not codes) and therefore pass them on unchanged to getTransformFromProjections.

@mike-000
Copy link
Contributor Author

mike-000 commented Apr 4, 2024

@ahocevar This is ready for further review. Workarounds have been removed in favour of the latest geotiff.js methods. The handling of matrixes in projections now uses entire transform functions (in series if necessary) instead of very inefficiently extracting a single coordinate transform from a projection transform function to use in a new transform function. This has made the rendering noticeably faster. In many cases (as in the two examples and current tests) the matrix transform is independent of projection transforms and could potentially be cached and/or used directly, e.g.

  if ((!sourceTransform || !destinationTransform) && !transformFunc) {
    return sourceTransform || destinationTransform;
  }

although I suspect there would be little further performance gain from this.

Matrix and projection transforms used in series will be needed for generic viewers such as attempted in https://radiantearth.github.io/stac-browser/#/external/s3.us-west-2.amazonaws.com/umbra-open-data-catalog/stac/2023/2023-02/2023-02-12/6d584f33-0489-47dd-9412-14d2c83532fc/6d584f33-0489-47dd-9412-14d2c83532fc.json where the rotated UTM is further transformed to 3857 as in https://jsfiddle.net/w4qxs50k/ - a test is still needed for that (potentially using an ImageStatic as in #7255 (comment)).

},
);
}
return (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single transform could be used directly here, but any performance gain from the extra code would be minimal

  if ((!sourceTransform || !destinationTransform) && !transformFunc) {
    return sourceTransform || destinationTransform;
  }

Copy link
Member

@ahocevar ahocevar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your continued effort here, @mike-000. The one thing that still does not look right is that you're using Projection as container for the transform matrix, and the projection transform function to apply a transform matrix. Like I said already, that does not feel like the right place.

Tthinking about it more, my previous suggestion to use transform functions directly does not feel right.

The GeoTIFF source looks like the right place to maintain a mapping image -> matrix, and to make use of it in an overridden getTile() method.

@mike-000
Copy link
Contributor Author

mike-000 commented Apr 4, 2024

Separating matrix and projection transforms has the potential for double reprojection. I would regard the image as being in a rotated version of a standard projection - in the example case a UTM projection - defined by a standard projection code and a rotation matrix, so to render it in for example EPSG:3857 only one reprojection should be needed (no reprojection of reproj tiles) the transform function being a combination of the UTM to 3857 transform and the matrix transform which can be easily done by getTransformFromProjections using only its current two parameters (and if source and destination codes are the same only the matrix is used). Having the overall projection defined by a code in the projection and a matrix in the source would be difficult for the current renderer to manage.

Similarly the technique could be used for KML ground overlays, by regarding them as being in rotated/scaled version of EPSG:4326, they could be easily rendered in any view projection which has a transform to EPSG:4326.

But I do think setting a matrix on a projection object should not be exposed in the API, it should be done only inside the GeoTIFF source code (and potentially inside a ground overlay parser) to avoid any future breaking changes.

@ahocevar
Copy link
Member

ahocevar commented Apr 4, 2024

I agree that a single transform of raster data is preferable. But still, this can be done in ol/source/DataTile#getReprojTile_(), if the ReporjDataTile is configured with a transform and a projection.

@mike-000
Copy link
Contributor Author

mike-000 commented Apr 8, 2024

A solution which is not specific to DataTile would be preferable, so it could be easily reused elsewhere, e.g. as part of a fix for #4949/#7255 where rotation transforms can be combined projection transforms without the need to maintain large numbers of individual transforms for multiple projection code/rotation permutations.

Also reprojection inevitably reduces the output quality, sometimes as in #15579 that can be unacceptable, so it would nice to allow the view to be in the tiles native projection and reproject any less important background or vector data. Currently getView() returns a standard projection familiar to the user, and the fact that the source may have a non-standard projection with the same code is not documented, but for best quality that can be used as the view projection as in https://jsfiddle.net/jo9wav6y/ something I think should be retained and documented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants