Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions examples/processes/mapterhorn.yaml
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]
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@
<artifactId>osm4j-geometry</artifactId>
<version>${osm4j.version}</version>
</dependency>
<!-- TwelveMonkeys library for WebP support in ImageIO -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>3.9.4</version>
</dependency>
Comment on lines +203 to +208

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.


<!--
TEST DEPENDENCIES
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/fr/ign/voxatile/core/Voxatile.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import fr.ign.voxatile.core.parameters.providers.ShapefileProviderParams;
import fr.ign.voxatile.core.parameters.providers.WFSProviderParams;
import fr.ign.voxatile.core.parameters.providers.WMSFloatBilProviderParams;
import fr.ign.voxatile.core.parameters.providers.XYZTilesProviderParams;
import fr.ign.voxatile.core.parameters.tasks.BuildLayoutTaskParams;
import fr.ign.voxatile.core.parameters.tasks.CopyHeightmapTaskParams;
import fr.ign.voxatile.core.parameters.tasks.FetchDataTaskParams;
Expand Down Expand Up @@ -139,6 +140,7 @@ public static void main(String[] args) throws FactoryException, InterruptedExcep
parser.registerParams("renderFacades", RenderFacadesTaskParams.class);
parser.registerParams("buildLayout", BuildLayoutTaskParams.class);

parser.registerParams("xyz", XYZTilesProviderParams.class);
parser.registerParams("wfs", WFSProviderParams.class);
parser.registerParams("gpkg", GeoPackageProviderParams.class);
parser.registerParams("shapefile", ShapefileProviderParams.class);
Expand Down
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

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.

Move this computation to provider ?
Compute cellSize[X-Y] and offset[X-Y] to provide in XYZTilesProvider ?


@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

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.

This too.

}
99 changes: 99 additions & 0 deletions src/main/java/fr/ign/voxatile/core/inputs/XYZTilesProvider.java
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
Expand Up @@ -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();

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

this.bbox = new WorldBBox2d(
mapToWorld.convert(new MapCoordinates(data.offsetX(), data.offsetY())),
Expand All @@ -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

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


// Using separate ceil & floor allows a good management of integer coordinates
int xf = (int) Math.floor(x);
Expand Down
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();
}
}
Loading