Repository navigation
XYZ Tiles provider #209
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
base: main
Are you sure you want to change the base?
XYZ Tiles provider #209
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| heightmaps: | ||
| ground: | ||
| default: 0 | ||
|
|
||
| forEachTile: | ||
|
|
||
| altitude: | ||
| type: sequence | ||
| do: | ||
| - type: fetchData | ||
| modelType: altitude | ||
| provider: | ||
| type: xyz | ||
| url: https://tiles.mapterhorn.com/{z}/{x}/{y}.webp | ||
| zoom: 16 | ||
| - type: populateHeightmap | ||
| models: | ||
| type: altitude | ||
| heightmap: ground | ||
| - type: setSpawn | ||
| heightmap: | ||
| sum: [ground, 1] | ||
| x: 0 | ||
| y: 0 | ||
| - type: renderHeightmap | ||
| at: ground | ||
| place: | ||
| structure: | ||
| - place: *voxel-grass | ||
| at: [0, 0, 0] | ||
| - place: *voxel-dirt | ||
| at: [0, 0, -3..-1] | ||
| - place: *voxel-stone | ||
| at: [0, 0, -20..-4] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package fr.ign.voxatile.core.inputs; | ||
|
|
||
| import java.awt.image.BufferedImage; | ||
|
|
||
| import fr.ign.voxatile.core.inputs.decoders.ElevationDecoder; | ||
|
|
||
| /** | ||
| * Represents a matrix of floating-point geographic data backed by a BufferedImage. | ||
| * | ||
| * @param image the backing BufferedImage | ||
| * @param format the elevation decoder format | ||
| * @param cellSizeX the size of a cell in the X direction | ||
| * @param cellSizeY the size of a cell in the Y direction | ||
| * @param offsetX the X offset of the matrix | ||
| * @param offsetY the Y offset of the matrix | ||
| */ | ||
| public record FloatBufferedImageMatrix( | ||
| BufferedImage image, | ||
| ElevationDecoder format, | ||
| double cellSizeX, | ||
| double cellSizeY, | ||
| double offsetX, | ||
| double offsetY | ||
| ) implements FloatGeographicDataMatrix2d { | ||
|
|
||
| /** | ||
| * Creates a {@code FloatBufferedImageMatrix} for the given XYZ tile. | ||
| * | ||
| * @param image the tile image | ||
| * @param format the elevation decoder format | ||
| * @param x the tile x-coordinate | ||
| * @param y the tile y-coordinate | ||
| * @param zoom the tile zoom level | ||
| * @return a new {@code FloatBufferedImageMatrix} for the given tile | ||
| */ | ||
| public static FloatBufferedImageMatrix forTile(BufferedImage image, ElevationDecoder format, int x, int y, int zoom) { | ||
| BoundingBox bbox = BoundingBox.fromTile(x, y, zoom); | ||
| double cellSizeX = (bbox.maxX() - bbox.minX()) / (image.getWidth() - 1); | ||
| double cellSizeY = (bbox.maxY() - bbox.minY()) / (image.getHeight() - 1); | ||
| double offsetX = bbox.minX() + (cellSizeX / 2.0); | ||
| double offsetY = bbox.maxY() - (cellSizeY / 2.0); | ||
|
|
||
| return new FloatBufferedImageMatrix(image, format, cellSizeX, cellSizeY, offsetX, offsetY); | ||
| } | ||
|
Comment on lines
+26
to
+44
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this computation to provider ? |
||
|
|
||
| @Override | ||
| public int sizeX() { | ||
| return image.getWidth(); | ||
| } | ||
|
|
||
| @Override | ||
| public int sizeY() { | ||
| return image.getHeight(); | ||
| } | ||
|
|
||
| @Override | ||
| public float getFloat(int x, int y) { | ||
| int rgb = image.getRGB(x, image.getHeight() - 1 - y); | ||
| return format.decode(rgb); | ||
| } | ||
|
|
||
| // FIXME: I'm not sure to keep this record, uniformise this calculs with calculs from XYZTilesProvider ? | ||
| // tile2lon(x, z) -> lon and lon2tile(lon, z) -> x | ||
| // tile2lat(y, z) -> lat and lat2tile(lat, z) -> y | ||
| private record BoundingBox(double minX, double minY, double maxX, double maxY) { | ||
|
|
||
| static BoundingBox fromTile(int x, int y, int zoom) { | ||
| return new BoundingBox( | ||
| tile2lon(x, zoom), | ||
| tile2lat(y + 1, zoom), | ||
| tile2lon(x + 1, zoom), | ||
| tile2lat(y, zoom) | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
| the formulas below are based on the Slippy map tilenames convention | ||
| https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames | ||
| */ | ||
| static double tile2lon(int x, int z) { | ||
| return x * 360.0 / (1 << z) - 180.0; | ||
| } | ||
|
|
||
| static double tile2lat(int y, int z) { | ||
| return Math.toDegrees(Math.atan(Math.sinh(Math.PI * (1 - 2.0 * y / (1 << z))))); | ||
| } | ||
| } | ||
|
Comment on lines
+62
to
+87
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This too. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package fr.ign.voxatile.core.inputs; | ||
|
|
||
| import java.awt.image.BufferedImage; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.net.URI; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import javax.imageio.ImageIO; | ||
|
|
||
| import org.geotools.api.referencing.FactoryException; | ||
| import org.geotools.api.referencing.crs.CoordinateReferenceSystem; | ||
| import org.geotools.geometry.jts.ReferencedEnvelope; | ||
| import org.geotools.referencing.CRS; | ||
|
|
||
| import fr.ign.voxatile.core.exceptions.GenerationFailedException; | ||
| import fr.ign.voxatile.core.exceptions.RetryableException; | ||
| import fr.ign.voxatile.core.exceptions.TransformException; | ||
| import fr.ign.voxatile.core.utils.coordinates.EnvelopeProvider; | ||
| import fr.ign.voxatile.core.utils.world3d.WorldBBox3d; | ||
| import fr.ign.voxatile.core.inputs.decoders.ElevationDecoder; | ||
|
|
||
| /** | ||
| * Data provider reading images from generic XYZ URL endpoints. | ||
| */ | ||
| public class XYZTilesProvider implements Provider<FloatGeographicDataMatrix2d> { | ||
|
|
||
| private final String url; | ||
| private final int zoom; | ||
| private final ElevationDecoder format; | ||
| private final EnvelopeProvider envelopeProvider; | ||
|
|
||
| /** | ||
| * Creates a new {@code XYZTilesProvider}. | ||
| * | ||
| * @param url URL with {@code {z}}, {@code {x}}, and {@code {y}} placeholders | ||
| * @param zoom zoom level for the tiles | ||
| * @param format the elevation decoder format for the tiles | ||
| * @param envelopeProvider the provider used to compute the envelope for the given CRS and bounding box | ||
| */ | ||
| public XYZTilesProvider( | ||
| String url, | ||
| int zoom, | ||
| ElevationDecoder format, | ||
| EnvelopeProvider envelopeProvider | ||
| ) { | ||
| this.url = url.replace("{z}", String.valueOf(zoom)); | ||
| this.zoom = zoom; | ||
| this.format = format; | ||
| this.envelopeProvider = envelopeProvider; | ||
| } | ||
|
|
||
| @Override | ||
| public Class<FloatGeographicDataMatrix2d> providedType() { | ||
| return FloatGeographicDataMatrix2d.class; | ||
| } | ||
|
|
||
| @Override | ||
| public Result<FloatGeographicDataMatrix2d> provide(WorldBBox3d bbox) throws GenerationFailedException, RetryableException { | ||
| CoordinateReferenceSystem crs; | ||
| ReferencedEnvelope envelope; | ||
| try { | ||
| crs = CRS.decode("EPSG:4326", true); | ||
| envelope = envelopeProvider.computeForCRS(crs, bbox); | ||
| } catch (FactoryException | TransformException e) { | ||
| throw new GenerationFailedException(e); | ||
| } | ||
|
|
||
| /* | ||
| Compute the tile coordinates (minX, minY, maxX, maxY) that cover the given envelope. | ||
| the formulas below are based on the Slippy map tilenames convention | ||
| https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames | ||
| */ | ||
| int minX = (int) Math.floor((envelope.getMinX() + 180) / 360 * (1<<zoom)); | ||
| double minLatRad = Math.toRadians(envelope.getMaxY()); | ||
| int minY = (int) Math.floor((1 - Math.log(Math.tan(minLatRad) + 1 / Math.cos(minLatRad)) / Math.PI) / 2 * (1 << zoom)); | ||
|
|
||
| int maxX = (int) Math.ceil((envelope.getMaxX() + 180) / 360 * (1<<zoom)); | ||
| double maxLatRad = Math.toRadians(envelope.getMinY()); | ||
| int maxY = (int) Math.ceil((1 - Math.log(Math.tan(maxLatRad) + 1 / Math.cos(maxLatRad)) / Math.PI) / 2 * (1 << zoom)); | ||
|
|
||
| List<FloatGeographicDataMatrix2d> tiles = new ArrayList<>(); | ||
| for (int y = minY; y <= maxY; y++) | ||
| for (int x = minX; x <= maxX; x++) { | ||
| BufferedImage image; | ||
| try (InputStream stream = URI.create(url.replace("{x}", String.valueOf(x)) | ||
| .replace("{y}", String.valueOf(y))).toURL().openStream()) { | ||
| image = ImageIO.read(stream); | ||
| } catch (IOException e) { | ||
| throw new GenerationFailedException(e); | ||
| } | ||
|
|
||
| if (image != null) | ||
| tiles.add(FloatBufferedImageMatrix.forTile(image, format, x, y, zoom)); | ||
| } | ||
|
|
||
| return new SimpleResult<>(crs, tiles.iterator()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package fr.ign.voxatile.core.inputs.decoders; | ||
|
|
||
| /** | ||
| * Interface for decoding RGB values to float elevation. | ||
| */ | ||
| public interface ElevationDecoder { | ||
| float decode(int rgb); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package fr.ign.voxatile.core.inputs.decoders; | ||
|
|
||
| /** | ||
| * Decoder for Terrarium format RGB values to float elevation. | ||
| */ | ||
| public class TerrariumElevationDecoder implements ElevationDecoder { | ||
|
|
||
| @Override | ||
| public float decode(int rgb) { | ||
| int r = (rgb >> 16) & 0xFF; | ||
| int g = (rgb >> 8) & 0xFF; | ||
| int b = rgb & 0xFF; | ||
| return (r * 256.0f + g + b / 256.0f) - 32768.0f; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,8 +35,8 @@ public FloatMatrixModel(FloatGeographicDataMatrix2d data, MapToWorldConverter co | |
|
|
||
| this.data = data; | ||
|
|
||
| double maxX = data.offsetX() + data.sizeX() * data.cellSizeX() - 1.0; | ||
| 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(); | ||
|
|
||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hotfix for WG84 CRS |
||
| this.bbox = new WorldBBox2d( | ||
| mapToWorld.convert(new MapCoordinates(data.offsetX(), data.offsetY())), | ||
|
|
@@ -63,8 +63,8 @@ public Float get(WorldCoords2d coords) { | |
| } | ||
|
|
||
| // We interpolate between cell centers (not cell upper left corner) | ||
| float x = (float) ((coordinates.x() + 0.5 - data.offsetX()) / data.cellSizeX() - 0.5); | ||
| float y = (float) ((coordinates.y() + 0.5 - data.offsetY()) / data.cellSizeY() - 0.5); | ||
| float x = (float) ((coordinates.x() - data.offsetX()) / data.cellSizeX()); | ||
| float y = (float) ((coordinates.y() - data.offsetY()) / data.cellSizeY()); | ||
|
Comment on lines
+66
to
+67
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hotfix for WG84 CRS |
||
|
|
||
| // Using separate ceil & floor allows a good management of integer coordinates | ||
| int xf = (int) Math.floor(x); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package fr.ign.voxatile.core.parameters.providers; | ||
|
|
||
| import java.beans.ConstructorProperties; | ||
|
|
||
| import fr.ign.voxatile.core.generation.Generation; | ||
| import fr.ign.voxatile.core.inputs.Provider; | ||
| import fr.ign.voxatile.core.inputs.FloatGeographicDataMatrix2d; | ||
| import fr.ign.voxatile.core.inputs.XYZTilesProvider; | ||
| import fr.ign.voxatile.core.inputs.decoders.TerrariumElevationDecoder; | ||
| import fr.ign.voxatile.core.parameters.processors.ProcessorParams; | ||
| import fr.ign.voxatile.core.parameters.processors.FloatMatrixProcessorParams; | ||
|
|
||
| /** | ||
| * Parameters for {@link XYZTilesProvider}. | ||
| */ | ||
| public class XYZTilesProviderParams extends ProviderParams { | ||
| /** | ||
| * URL carrying the {@code {z}}, {@code {x}} and {@code {y}} placeholders (required). | ||
| */ | ||
| public String url; | ||
|
|
||
| /** | ||
| * Zoom level for the tiles (required). | ||
| */ | ||
| public int zoom; | ||
|
|
||
| // TODO: field for elevation decoder format | ||
| // public ElevationDecoderParams format; | ||
|
|
||
| /** | ||
| * Constructs a new {@link XYZTilesProviderParams} with mandatory fields. | ||
| * | ||
| * @param url URL carrying the {@code {z}}, {@code {x}} and {@code {y}} placeholders | ||
| * @param zoom zoom level for the tiles | ||
| */ | ||
| @ConstructorProperties({"url", "zoom"}) | ||
| public XYZTilesProviderParams(String url, int zoom) { | ||
| this.url = url; | ||
| this.zoom = zoom; | ||
| } | ||
|
|
||
| @Override | ||
| public Provider<FloatGeographicDataMatrix2d> create(Generation generation) { | ||
| if (url == null || url.isBlank()) | ||
| throw new IllegalArgumentException("An XYZ tiles provider needs a url"); | ||
| if (!url.contains("{z}") || !url.contains("{x}") || !url.contains("{y}")) | ||
| throw new IllegalArgumentException("The url must carry the {z}, {x} and {y} placeholders, got \"%s\"".formatted(url)); | ||
|
|
||
| // TODO: Handle more elevation decoder formats | ||
| return new XYZTilesProvider(url, zoom, new TerrariumElevationDecoder(), generation::getEnvelopeForCRS); | ||
| } | ||
|
|
||
| @Override | ||
| public ProcessorParams defaultProcessor() { | ||
| return new FloatMatrixProcessorParams(); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.