Skip to content

XYZ Tiles provider - #209

Open
naulan-chrzaszcz wants to merge 3 commits into
mainfrom
naulan/terrarium-provider
Open

naulan-chrzaszcz wants to merge 3 commits into
mainfrom
naulan/terrarium-provider

Conversation

@naulan-chrzaszcz

@naulan-chrzaszcz naulan-chrzaszcz commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Changes

Add a new provider which handle a new data source which provide images given the elevations with special encoding and with XYZ Tiles endpoint.

Technical changes

Add a new provider XYZTilesProvider with her parameters, which download images and store it into a new FloatBufferedImageMatrix model which can handle many format to get elevations.

This model can only supported (for now) the terrarium format handle by TerrariumElevationDecoder class.

Showcase

image

Reason

For Albanian and world wide DTM rendering.

TODOs

  • Documentation
  • Unit tests

Self-checks

  • The code has unit tests associated
  • The code has Javadoc Comments associated
  • Complex / Unexpected code is explained / justified with a small comment
  • Relevant documentation inside the /docs folder has been updated
  • All examples in examples/ work the same (or have been adapted if subject to changes in this PR)
  • Git history is clean (each commit accomplish a single task and describe it accordingly)
  • The texts have been proofread (documentation, error messages, logs, comments...)

@naulan-chrzaszcz

Copy link
Copy Markdown
Member Author

This feature is pretty raw; it was a prototype I quickly put together while I was in Albania, using the most powerful Claude Code model.
I need to make it less obscure and understand what it actually generated for me.

@naulan-chrzaszcz naulan-chrzaszcz added type: enhancement New feature or request size: medium Neither small nor big change, will require some time to review On hold Todo later labels Sep 24, 2026
@naulan-chrzaszcz naulan-chrzaszcz self-assigned this Sep 24, 2026
@indyteo

indyteo commented Sep 24, 2026

Copy link
Copy Markdown
Member

I did not read the code in details, but just from a quick overview, it seems like you can quite drastically simplify it, especially by removing the re-projection.

In the WMS provider, we have a CRS in the params because we need to tell to the GeoServer in which CRS we want our data. It is intended that the user inputs either the native data CRS (so the server don't reproject and our MapToWorldConverter does), or the generated map CRS (so the server reprojects and our converter don't).
In the Overpass provider, we don't have a CRS in the params because OSM data is ALWAYS in EPSG:4326. Our MapToWorldConverter will handle the re-projection as long as the correct CRS is indicated in the Provider.Result#crs() method.

In your case, it seems like the data will always be in EPSG:4326. So, just like the Overpass provider, don't worry about any re-projection yourself, and let the MapToWorldConverter do it. I guess you started from the WMS provider, which explains why you had this CRS and thus struggled to integrate it.

Additionally, keep in mind that it is completely OK for a provider to return multiple results. Even with matrices, it looks like you could return one matrix per tile instead of having to handle the merging yourself. Claude might be good at writing working code, but it's up to you to integrate it properly into the codebase (because it doesn't know about everything that is already handled by another component). If you were to write such code by hand, would you have really implemented re-projection and tile merging yourself? Probably not, because you would have known that it was tedious to do and also unnecessary ;)

@pyrollo

pyrollo commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

This feature is interesting.

You should not limit it to Terrarium, many other data are provided using XYZ tiles.

And as said Theo, this could be very simplified and made more Voxatile-like:

  • separate Provider logic from data storage logic (keep data in BufferedImage);
  • remove useless (and memory consuming) conversion to FloatGeographicDataMatrix2d, prefer keeping data in BufferedImage rather (this is the policy we had from the beginning of the project);
  • avoid useless CRS transformations

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

[Maven Build Status]

📑 Commit: b05c64e9d13f40835b93d28ad4c084cf00843582
⌚️ Date: 2026-11-01T17:47:56 (CET)
🛠️ Status: ✅ Success

📦 Download artifact: voxatile.jar

@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch 3 times, most recently from f0860db to 95bae7f Compare October 8, 2026 15:23
@naulan-chrzaszcz naulan-chrzaszcz changed the title Terrarium tiles provider XYZ Tiles provider Oct 8, 2026
@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch 2 times, most recently from 5f62b48 to ec221a7 Compare October 8, 2026 15:28
double maxY = data.offsetY() + data.sizeY() * data.cellSizeY() - 1.0;
double maxX = data.offsetX() + (data.sizeX() - 1) * data.cellSizeX();
double maxY = data.offsetY() + (data.sizeY() - 1) * data.cellSizeY();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hotfix for WG84 CRS

Comment on lines +66 to +67
float x = (float) ((coordinates.x() - data.offsetX()) / data.cellSizeX());
float y = (float) ((coordinates.y() - data.offsetY()) / data.cellSizeY());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hotfix for WG84 CRS

Comment thread pom.xml
Comment on lines +203 to +208
<!-- TwelveMonkeys library for WebP support in ImageIO -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>3.9.4</version>
</dependency>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The new endpoint of Mapterhorn use WebP.

@naulan-chrzaszcz naulan-chrzaszcz removed the On hold Todo later label Oct 8, 2026
@naulan-chrzaszcz
naulan-chrzaszcz marked this pull request as ready for review October 8, 2026 15:43
@naulan-chrzaszcz

Copy link
Copy Markdown
Member Author

Feel free to give some reviews if you'd like, @ignfab/minalac
But the code can change fastly.

@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch from ec221a7 to e2c12fe Compare October 9, 2026 13:41
@naulan-chrzaszcz

naulan-chrzaszcz commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author
image Est-ce que les platistes auraient raison, finalement ? P'tit problème de rendu...

@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch from e2c12fe to 67fdc42 Compare October 9, 2026 13:53
Comment thread docs/usage/parameters/FetchDataTask.md Outdated

**Extra parameters**:
- `url` (required): URL containing the `{z}`, `{x}`, and `{y}` placeholders. Example : `https://tiles.mapterhorn.com/{z}/{x}/{y}.webp`
- `zoom` (required): Zoom level for the tiles (must be a non-negative integer)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why we have to specify that ? What zoom value should I put ?

Voxatile should know better than me what zoom to use.

@indyteo

indyteo commented Oct 12, 2026

Copy link
Copy Markdown
Member

Okay, this is promising. Let's see some high-level considerations first.

So, after reading your code, here is how I see it:
You have a provider dedicated to XYZ tiles querying, crafting requests from a user-defined URL that implies the tiles format (PNG, JPEG, WebP...). This format does not matter, as long as Java can read it into a standard buffered image (which is nice, because it was designed to be extensible, meaning that additional formats can be supported, as shown with WebP).
Because each tile needs to be independent, you must wrap that buffered image into a geographic data matrix. Because you are working with altitude data, you made a new implementation of float geographic data matrix, based on a buffered image. But a buffered image typically embeds integers, not floats, so you added an integer to float function to convert from one into another.
Then, you added a single implementation based on the Terrarium encoding algorithm.
At this point, you are done since those float geographic data matrices can be processed by the already existing float matrix processor.

First of all, is that correct? Let me know if I missed something important.

Now, when I think about this topic, I see a different approach:
We make an XYZ tiles provider similar to yours, but instead providing integer geographic data matrices (new implementation, will probably be required anyway at some point to handle color images as well), thus without any conversion needed.
Then, we create a new processor, dedicated to Terrarium, which create float matrix models from those integer geographic data matrices (with the good implementation, no data copy nor extensive code duplication is needed).
It would then work the same because that processor would've created float matrix models.

So, now I have a few questions for you:

  1. Do you understand the approach I'm talking about?
  2. What do you think of your current approach? What are the differences with this one?
  3. What are the pros and cons of the two, and ultimately, which one do you think would be the best?

Please note that I'm asking you to write a message with your answers, not to code anything right now!
And obviously, I'm asking you to think about it, as a "design exercise", I'm not prompting Claude to do it 😉

@pyrollo

pyrollo commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Please note that I'm asking you to write a message with your answers, not to code anything right now!

That's important to say :D

Your proposal is interesting (and promising). But before that, I think there is a serious issue with zoom that should be fixed.

@indyteo

indyteo commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

But before that, I think there is a serious issue with zoom that should be fixed.

Indeed, this is completely unrelated. Whatever the global design is, the same issues about the actual XYZ providing logic will need to be considered, such as the zoom parameter you mentioned, or the tiles streaming instead of buffering... but those can be handled separately, either before, in parallel, or after.

Considering I (only) asked for critical thinking and analysis, I think it can be performed in parallel to addressing initial code reviews, yes!

@indyteo

indyteo commented Oct 13, 2026

Copy link
Copy Markdown
Member

Just a side note regarding the analysis I asked you, if it's easier for you, I don't mind if you do it in French. Doing it in English would be a good exercise as well, but I prefer you focus on quality of content rather than language.

@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch from ca8e6b2 to 6930da0 Compare October 27, 2026 08:31
@naulan-chrzaszcz

Copy link
Copy Markdown
Member Author

Okay, this is promising. Let's see some high-level considerations first.

So, after reading your code, here is how I see it: You have a provider dedicated to XYZ tiles querying, crafting requests from a user-defined URL that implies the tiles format (PNG, JPEG, WebP...). This format does not matter, as long as Java can read it into a standard buffered image (which is nice, because it was designed to be extensible, meaning that additional formats can be supported, as shown with WebP). Because each tile needs to be independent, you must wrap that buffered image into a geographic data matrix. Because you are working with altitude data, you made a new implementation of float geographic data matrix, based on a buffered image. But a buffered image typically embeds integers, not floats, so you added an integer to float function to convert from one into another. Then, you added a single implementation based on the Terrarium encoding algorithm. At this point, you are done since those float geographic data matrices can be processed by the already existing float matrix processor.

First of all, is that correct? Let me know if I missed something important.

Now, when I think about this topic, I see a different approach: We make an XYZ tiles provider similar to yours, but instead providing integer geographic data matrices (new implementation, will probably be required anyway at some point to handle color images as well), thus without any conversion needed. Then, we create a new processor, dedicated to Terrarium, which create float matrix models from those integer geographic data matrices (with the good implementation, no data copy nor extensive code duplication is needed). It would then work the same because that processor would've created float matrix models.

So, now I have a few questions for you:

1. Do you understand the approach I'm talking about?

2. What do you think of your current approach? What are the differences with this one?

3. What are the pros and cons of the two, and ultimately, which one do you think would be the best?

Please note that I'm asking you to write a message with your answers, not to code anything right now! And obviously, I'm asking you to think about it, as a "design exercise", I'm not prompting Claude to do it 😉

Okay, your approach is interesting.

You're right, I've a provider dedicated to querying XYZ tiles, which builds requests based on a user-defined URL. And yeah, each tile is stored in a geographic data matrix using a buffered image.

Then, I've added a single implementation based on Terrarium encoding to convert RGB integer values into floating-point altitude values. And that's it, I'm done.

Your new approach is interesting because my current provider is specifically designed for elevation data, as I'm working with the Mapterhorn endpoint. This is my big mistake, I didn't increase my FOV for this provider. My FOV was set to 30°. I need time to set to Quake Pro.
image

However, it's possible to extend this concept to make it more general-purpose rather than keeping it specific to elevation data. That's actually something we've been trying to achieve since the beginning of the Voxatile source code design.

Mmmh, with my current implementation, the class name is more like XYZTilesAltitudesProvider.java.

Idk if I've given you a good answer.

@naulan-chrzaszcz

Copy link
Copy Markdown
Member Author

Just a side note regarding the analysis I asked you, if it's easier for you, I don't mind if you do it in French. Doing it in English would be a good exercise as well, but I prefer you focus on quality of content rather than language.

Okay, i did it in english with a fix with ChatGPT for some mistake.
i tried, but if isn't enough about detail of my analytics, i can do a new answer in French.

Don't be mean with me.

@naulan-chrzaszcz

Copy link
Copy Markdown
Member Author

But before that, I think there is a serious issue with zoom that should be fixed.

Indeed, this is completely unrelated. Whatever the global design is, the same issues about the actual XYZ providing logic will need to be considered, such as the zoom parameter you mentioned, or the tiles streaming instead of buffering... but those can be handled separately, either before, in parallel, or after.

Considering I (only) asked for critical thinking and analysis, I think it can be performed in parallel to addressing initial code reviews, yes!

What do you mean by "tiles streaming instead of buffering" ? Do you have an example ?

@indyteo

indyteo commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Thanks for the answer, it's a good start. However, I don't really see the answers to questions 2 and 3 (especially 3, question 2 is more to help you to understand).

Since you started to code it, you can try to compare your two attempts (initial version vs new version). Don't forget to be critical (i.e. point out what you think is good vs what is bad!).

Please try not to use LLMs, even if that means doing it in French, it's not a problem! ;)

Regarding your last question, currently you do N requests (1 for each tile), accumulating tiles in a collection, then return the result with an iterator over that collection. This loads everything before the first tile can be processed.
Instead, you could immediately return a result those "next" method will do the request for the next tile. At the end, it'll be the same, everything will be loaded. But processing could be done as soon as each tile is loaded.

@naulan-chrzaszcz

naulan-chrzaszcz commented Nov 1, 2026 •

Copy link
Copy Markdown
Member Author

Thanks for the answer, it's a good start. However, I don't really see the answers to questions 2 and 3 (especially 3, question 2 is more to help you to understand).

Since you started to code it, you can try to compare your two attempts (initial version vs new version). Don't forget to be critical (i.e. point out what you think is good vs what is bad!).

Version Pros Cons
Initial version La version permettait de faire une simple implementation d'interface pour crée le nouveau model qui permettait de stocker l'image dans cette interface pour que tout fonctionne comme avant. Mais, on perd en généricité, le provider avait pour unique objectif de transformer les images qu'il recoit en un tableau de float, limitant donc, d'autre cas d'usage.

Il manquait la séparation des tâches, le provider ne fesait pas que recolter les tuiles et le ranger, il le recolter, le converti, et le range.
New version Le nouveau code permet de rendre donc générique le provider.

Le nouveau code permet une meilleurs séparation des responsabilitées, le provider maintenant recolte et range directement l'image dans une interface approprié, et ne bride pas le provider a un seul cas d'usage qui etait l'elevation. Le Processor permet de decoder l'image et le ranger dans le bon type de model a la fin.
Crée plus d'interface pour permettre de supporter juste un tableau de Integer au lieu du float initialement codé.

Il y a deux interface en plus, une qui permet de declarer une matrice d'integer qui expose getInteger et l'autre, une interface qui permet de stocker l'image et d'exposer ses valeurs RGB avec le getInteger.

Donc, je préfère la nouvelle version, car, comme expliqué dans le tableau que j'ai créé, le provider n'est pas bridé à un seul cas d'usage, et permet, avec juste l'ajout d'autres paramètres, de créer d'autres cas d'usage de ce provider.

@naulan-chrzaszcz

Copy link
Copy Markdown
Member Author

not to code anything right now!

Bon, tu m'excuseras, j'ai quand même testé (et push) la version dont tu as parlé dans ton message, c'était obvious que c'était mieux.

@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch 2 times, most recently from 4715b0e to 32bc8e2 Compare November 1, 2026 16:45
@naulan-chrzaszcz
naulan-chrzaszcz force-pushed the naulan/terrarium-provider branch from 32bc8e2 to b05c64e Compare November 1, 2026 16:47
@indyteo

indyteo commented Nov 1, 2026

Copy link
Copy Markdown
Member

Okay, thank you for this analysis, I hope you understand the interest of doing it, and usefulness of comparing different solutions to find the most suitable one.

And in the future, I encourage you to do similar things, for example when writing your PR body text, in the "reason" section, try to state what key advantages made you choose that solution over others. You can take inspiration from my PRs where I try to do that (for example #172). But this is not something to always do, it's only useful when the choices are not obvious.

Anyways... Now, I'd like to point out something in your new version, to challenge it a little bit: The processor copies data from the integer matrix to a float matrix. What do you think about that?

@naulan-chrzaszcz

naulan-chrzaszcz commented Nov 1, 2026 •

Copy link
Copy Markdown
Member Author

You can take inspiration from my PRs where I try to do that (for example #172). But this is not something to always do, it's only useful when the choices are not obvious.

Thank you for the example!

The processor copies data from the integer matrix to a float matrix. What do you think about that?

Il copie pas vraiment la valeur vers la cible telle qu'il la reçoit.

Vu que le processor est responsable de la transformation d'une donnée traitable vers un model, il y a pas juste une simple copie, il y a de l'adaptation ou/et de la traduction.

Par exemple dans mon cas, je traduit mes valeurs RGB en Float. Donc, c'est pas juste un simple copie.

@indyteo

indyteo commented Nov 1, 2026

Copy link
Copy Markdown
Member

Ok, donc tu dis qu'il y a une transformation + une copie. Est-ce que ces deux opérations sont indissociables ? Est-ce que les deux sont nécessaires ?

La situation exacte est que : tu fais un processeur qui doit prendre en entrée des matrices géographiques d'entiers, et les transformer en modèles matrices de flottants. Dans un soucis d'optimisation, si on peut éviter de recopier des données, on préfère.

Est-ce que cette situation te parait similaire à une question que tu as déjà traité à un moment donné ? Est-ce que tu pourrais t'en inspirer pour trouver une solution pour celle-ci ?

area:
center:
latitude: -17.995
longitude: 178.000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Attention, ici tu ne verras pas le bug du "tour du monde" car +/- 1km ne te feront pas dépasser la barrière de 180°.

int maxY = latitudeToY(envelope.getMinY(), numTiles);

/*
* Handle International Date Line crossing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Date Line crossing ?

*/
int countX = (maxX >= minX) ? (maxX - minX + 1) : (numTiles - minX + maxX + 1);

List<IntegerGeographicDataMatrix2d> tiles = new ArrayList<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

C'est ici que tu fais le choix de tout lire avant de pouvoir traiter la première tuile. Ce qui n'est pas très optimisé.

}
}

return new SimpleResult<>(CRS, tiles.iterator());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Et là, tu retourne un itérateur (ce qui est demandé), mais on n'a l'itérateur qu'une fois la dernière tuile lue, alors que le premier next n'en a pas besoin.

* It mathematically breaks beyond ~85.0511 degrees latitude (Logarithm of zero/infinity).
* So we clamp the latitude to this exact maximum limit.
*/
double clampedLat = Math.max(-MAX_MERCATOR_LATITUDE, Math.min(MAX_MERCATOR_LATITUDE, lat));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Attention, ici au lieu de dire "je n'ai pas de données au delà de 85°", tu dis "Ok voilà les données" en revoyant une autre latitude.

Comment on lines +79 to +86
int tileSize;
try (InputStream stream = URI.create(url.replace("{z}", String.valueOf(zoomMin))
.replace("{x}", "0")
.replace("{y}", "0")).toURL().openStream()) {
tileSize = ImageIO.read(stream).getWidth();
} catch (Exception e) {
throw new GenerationFailedException(e);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Remplacer ca pour un fallback du zoom jusqu'à trouver celui qui fonctionne

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Il va y avoir une question de priorité de traitement des modèles dans le populateHeightmap ensuite puisque tu risques d'avoir des tuiles qui se recouvrent si tu as différents niveaux de zoom.

int x = (minX + stepX) % numTiles;

BufferedImage image;
try (InputStream stream = URI.create(url.replace("{x}", String.valueOf(x))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Before trying to fetch tile, it could be a good thing to check it actually intersects envelope. If not, proceed to next one, that will avoid useless network and memory consumption.

This branch has not been deployed

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

Labels

size: medium Neither small nor big change, will require some time to review type: enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants