Skip to content

Commit f94eb34

Browse files
committed
Add Chaikin smoother and improve hillshade and contour server
1 parent e3994b3 commit f94eb34

File tree

10 files changed

+360
-141
lines changed

10 files changed

+360
-141
lines changed

baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java renamed to baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Hillshade.java

Lines changed: 147 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,35 @@
3131
import com.linecorp.armeria.server.annotation.Param;
3232
import com.linecorp.armeria.server.cors.CorsService;
3333
import com.linecorp.armeria.server.docs.DocService;
34+
import com.linecorp.armeria.server.file.HttpFile;
3435
import java.awt.*;
3536
import java.awt.image.BufferedImage;
3637
import java.io.ByteArrayOutputStream;
3738
import java.io.IOException;
3839
import java.net.URL;
3940
import java.nio.ByteBuffer;
41+
import java.util.ArrayList;
42+
import java.util.List;
43+
import java.util.Map;
4044
import java.util.concurrent.Callable;
4145
import java.util.function.Function;
4246
import java.util.function.Supplier;
47+
import java.util.zip.GZIPOutputStream;
4348
import javax.imageio.ImageIO;
44-
import org.apache.baremaps.raster.elevation.ElevationUtils;
45-
import org.apache.baremaps.raster.elevation.HillshadeCalculator;
49+
import org.apache.baremaps.maplibre.vectortile.*;
50+
import org.apache.baremaps.raster.elevation.*;
51+
import org.apache.baremaps.server.TileResource;
4652
import org.apache.baremaps.tilestore.TileCoord;
4753
import org.apache.baremaps.tilestore.TileStore;
4854
import org.apache.baremaps.tilestore.TileStoreException;
55+
import org.locationtech.jts.geom.util.AffineTransformation;
4956
import org.slf4j.Logger;
5057
import org.slf4j.LoggerFactory;
5158
import picocli.CommandLine.Command;
5259
import picocli.CommandLine.Option;
5360

5461
@Command(name = "hillshade", description = "Start a tile server that computes hillshades.")
55-
public class HillShade implements Callable<Integer> {
62+
public class Hillshade implements Callable<Integer> {
5663

5764
@Option(names = {"--host"}, paramLabel = "HOST", description = "The host of the server.")
5865
private String host = "localhost";
@@ -68,11 +75,22 @@ public Integer call() throws Exception {
6875

6976
var objectMapper = objectMapper();
7077
var jsonResponseConverter = new JacksonResponseConverterFunction(objectMapper);
71-
var tileStore = new HillShadeTileStore();
7278

73-
serverBuilder.annotatedService(new HillShadeTileResource(() -> tileStore),
79+
LoadingCache<TileCoord, BufferedImage> cache = Caffeine.newBuilder()
80+
.maximumSize(1000)
81+
.build(this::getImage);
82+
83+
var rasterHillshadeTileStore = new RasterHillshadeTileStore(cache);
84+
serverBuilder.annotatedService(new HillShadeTileResource(() -> rasterHillshadeTileStore),
85+
jsonResponseConverter);
86+
87+
var contourTileStore = new ContourTileStore(cache);
88+
serverBuilder.annotatedService(new TileResource(() -> contourTileStore),
7489
jsonResponseConverter);
7590

91+
var index = HttpFile.of(ClassLoader.getSystemClassLoader(), "/raster/hillshade.html");
92+
serverBuilder.service("/", index.asService());
93+
7694
serverBuilder.decorator(CorsService.builderForAnyOrigin()
7795
.allowAllRequestHeaders(true)
7896
.allowRequestMethods(
@@ -102,63 +120,59 @@ public Integer call() throws Exception {
102120
return 0;
103121
}
104122

105-
public static class HillShadeTileStore implements TileStore {
123+
private String url = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
106124

107-
// private String url = "https://s3.amazonaws.com/elevation-tiles-prod/geotiff/{z}/{x}/{y}.tif";
108-
// private String url = "https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png";
109-
private String url = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
110-
111-
private final LoadingCache<TileCoord, BufferedImage> cache = Caffeine.newBuilder()
112-
.maximumSize(1000)
113-
.build(this::getImage);
125+
public BufferedImage getImage(TileCoord tileCoord) throws IOException {
126+
var tileUrl = new URL(this.url
127+
.replace("{z}", String.valueOf(tileCoord.z()))
128+
.replace("{x}", String.valueOf(tileCoord.x()))
129+
.replace("{y}", String.valueOf(tileCoord.y())));
130+
return ImageIO.read(tileUrl);
131+
}
114132

115-
public HillShadeTileStore() {
116-
// Default constructor
133+
public static BufferedImage getKernel(TileCoord tileCoord,
134+
Function<TileCoord, BufferedImage> provider) {
135+
BufferedImage z1 =
136+
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() - 1, tileCoord.z()));
137+
BufferedImage z2 =
138+
provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() - 1, tileCoord.z()));
139+
BufferedImage z3 =
140+
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() - 1, tileCoord.z()));
141+
BufferedImage z4 =
142+
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y(), tileCoord.z()));
143+
BufferedImage z5 = provider.apply(tileCoord);
144+
BufferedImage z6 =
145+
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y(), tileCoord.z()));
146+
BufferedImage z7 =
147+
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() + 1, tileCoord.z()));
148+
BufferedImage z8 =
149+
provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() + 1, tileCoord.z()));
150+
BufferedImage z9 =
151+
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() + 1, tileCoord.z()));
152+
int kernelSize = z5.getWidth() * 3;
153+
BufferedImage kernel = new BufferedImage(kernelSize, kernelSize, z5.getType());
154+
for (int y = 0; y < z5.getHeight(); y++) {
155+
for (int x = 0; x < z5.getWidth(); x++) {
156+
kernel.setRGB(x, y, z1.getRGB(x, y));
157+
kernel.setRGB(x + z5.getWidth(), y, z2.getRGB(x, y));
158+
kernel.setRGB(x + 2 * z5.getWidth(), y, z3.getRGB(x, y));
159+
kernel.setRGB(x, y + z5.getHeight(), z4.getRGB(x, y));
160+
kernel.setRGB(x + z5.getWidth(), y + z5.getHeight(), z5.getRGB(x, y));
161+
kernel.setRGB(x + 2 * z5.getWidth(), y + z5.getHeight(), z6.getRGB(x, y));
162+
kernel.setRGB(x, y + 2 * z5.getHeight(), z7.getRGB(x, y));
163+
kernel.setRGB(x + z5.getWidth(), y + 2 * z5.getHeight(), z8.getRGB(x, y));
164+
kernel.setRGB(x + 2 * z5.getWidth(), y + 2 * z5.getHeight(), z9.getRGB(x, y));
165+
}
117166
}
167+
return kernel;
168+
}
118169

119-
public BufferedImage getImage(TileCoord tileCoord) throws IOException {
120-
var tileUrl = new URL(this.url
121-
.replace("{z}", String.valueOf(tileCoord.z()))
122-
.replace("{x}", String.valueOf(tileCoord.x()))
123-
.replace("{y}", String.valueOf(tileCoord.y())));
124-
return ImageIO.read(tileUrl);
125-
}
170+
public static class RasterHillshadeTileStore implements TileStore {
126171

127-
public BufferedImage getKernel(TileCoord tileCoord, Function<TileCoord, BufferedImage> provider)
128-
throws IOException {
129-
BufferedImage z1 =
130-
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() - 1, tileCoord.z()));
131-
BufferedImage z2 =
132-
provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() - 1, tileCoord.z()));
133-
BufferedImage z3 =
134-
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() - 1, tileCoord.z()));
135-
BufferedImage z4 =
136-
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y(), tileCoord.z()));
137-
BufferedImage z5 = provider.apply(tileCoord);
138-
BufferedImage z6 =
139-
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y(), tileCoord.z()));
140-
BufferedImage z7 =
141-
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() + 1, tileCoord.z()));
142-
BufferedImage z8 =
143-
provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() + 1, tileCoord.z()));
144-
BufferedImage z9 =
145-
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() + 1, tileCoord.z()));
146-
int kernelSize = z5.getWidth() * 3;
147-
BufferedImage kernel = new BufferedImage(kernelSize, kernelSize, z5.getType());
148-
for (int y = 0; y < z5.getHeight(); y++) {
149-
for (int x = 0; x < z5.getWidth(); x++) {
150-
kernel.setRGB(x, y, z1.getRGB(x, y));
151-
kernel.setRGB(x + z5.getWidth(), y, z2.getRGB(x, y));
152-
kernel.setRGB(x + 2 * z5.getWidth(), y, z3.getRGB(x, y));
153-
kernel.setRGB(x, y + z5.getHeight(), z4.getRGB(x, y));
154-
kernel.setRGB(x + z5.getWidth(), y + z5.getHeight(), z5.getRGB(x, y));
155-
kernel.setRGB(x + 2 * z5.getWidth(), y + z5.getHeight(), z6.getRGB(x, y));
156-
kernel.setRGB(x, y + 2 * z5.getHeight(), z7.getRGB(x, y));
157-
kernel.setRGB(x + z5.getWidth(), y + 2 * z5.getHeight(), z8.getRGB(x, y));
158-
kernel.setRGB(x + 2 * z5.getWidth(), y + 2 * z5.getHeight(), z9.getRGB(x, y));
159-
}
160-
}
161-
return kernel;
172+
private final LoadingCache<TileCoord, BufferedImage> cache;
173+
174+
public RasterHillshadeTileStore(LoadingCache<TileCoord, BufferedImage> cache) {
175+
this.cache = cache;
162176
}
163177

164178
@Override
@@ -174,16 +188,16 @@ public ByteBuffer read(TileCoord tileCoord) throws TileStoreException {
174188
image.getHeight() + 2);
175189

176190
var grid = ElevationUtils.imageToGrid(buffer);
177-
var hillshade =
178-
new HillshadeCalculator(grid, buffer.getWidth(), buffer.getHeight(), 1, true)
191+
var hillshadeGrid =
192+
new HillshadeCalculator(grid, buffer.getWidth(), buffer.getHeight(), 1, false)
179193
.calculate(45, 315);
180194

181195
// Create an output image
182196
BufferedImage hillshadeImage =
183197
new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
184198
for (int y = 0; y < image.getHeight(); y++) {
185199
for (int x = 0; x < image.getWidth(); x++) {
186-
int value = (int) hillshade[(y + 1) * buffer.getHeight() + x + 1];
200+
int value = (int) hillshadeGrid[(y + 1) * buffer.getHeight() + x + 1];
187201
hillshadeImage.setRGB(x, y, new Color(value, value, value).getRGB());
188202
}
189203
}
@@ -229,7 +243,7 @@ public HillShadeTileResource(Supplier<TileStore> tileStoreSupplier) {
229243
this.tileStoreSupplier = tileStoreSupplier;
230244
}
231245

232-
@Get("regex:^/tiles/(?<z>[0-9]+)/(?<x>[0-9]+)/(?<y>[0-9]+).png")
246+
@Get("regex:^/raster/(?<z>[0-9]+)/(?<x>[0-9]+)/(?<y>[0-9]+).png")
233247
@Blocking
234248
public HttpResponse tile(@Param("z") int z, @Param("x") int x, @Param("y") int y) {
235249
TileCoord tileCoord = new TileCoord(x, y, z);
@@ -255,4 +269,76 @@ public HttpResponse tile(@Param("z") int z, @Param("x") int x, @Param("y") int y
255269
}
256270
}
257271
}
272+
273+
public static class ContourTileStore implements TileStore {
274+
275+
private final LoadingCache<TileCoord, BufferedImage> cache;
276+
277+
public ContourTileStore(LoadingCache<TileCoord, BufferedImage> cache) {
278+
this.cache = cache;
279+
}
280+
281+
@Override
282+
public ByteBuffer read(TileCoord tileCoord) throws TileStoreException {
283+
var image = cache.get(tileCoord);
284+
285+
var kernel = getKernel(tileCoord, cache::get);
286+
287+
image = kernel.getSubimage(
288+
image.getWidth() - 4,
289+
image.getHeight() - 4,
290+
image.getWidth() + 8,
291+
image.getHeight() + 8);
292+
293+
var grid = ElevationUtils.imageToGrid(image);
294+
295+
var features = new ArrayList<Feature>();
296+
297+
for (int level = 0; level < 9000; level += 100) {
298+
var contours = new ContourTracer(grid, image.getWidth(), image.getHeight(), false, false)
299+
.traceContours(level);
300+
301+
for (var contour : contours) {
302+
303+
contour = AffineTransformation
304+
.translationInstance(-4, -4)
305+
.scaleInstance(16, 16)
306+
.transform(contour);
307+
308+
// contour = new ChaikinSmoother(2, 0.25).transform(contour);
309+
310+
features.add(new Feature(level, Map.of("level", String.valueOf(level)), contour));
311+
}
312+
}
313+
314+
var layer = new Layer("elevation", 4096, features);
315+
var tile = new Tile(List.of(layer));
316+
var vectorTile = new VectorTileEncoder().encodeTile(tile);
317+
try (var baos = new ByteArrayOutputStream()) {
318+
var gzip = new GZIPOutputStream(baos);
319+
vectorTile.writeTo(gzip);
320+
gzip.close();
321+
return ByteBuffer.wrap(baos.toByteArray());
322+
} catch (IOException e) {
323+
throw new RuntimeException(e);
324+
}
325+
326+
}
327+
328+
@Override
329+
public void write(TileCoord tileCoord, ByteBuffer blob) throws TileStoreException {
330+
throw new UnsupportedOperationException();
331+
}
332+
333+
@Override
334+
public void delete(TileCoord tileCoord) throws TileStoreException {
335+
throw new UnsupportedOperationException();
336+
}
337+
338+
@Override
339+
public void close() throws Exception {
340+
// Do nothing
341+
}
342+
}
343+
258344
}

baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import picocli.CommandLine.Command;
2424

2525
@Command(name = "raster", description = "Raster processing commands.",
26-
subcommands = {HillShade.class},
26+
subcommands = {Hillshade.class},
2727
sortOptions = false)
2828
public class Raster implements Runnable {
2929

baremaps-core/src/main/java/org/apache/baremaps/tilestore/TileStore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020

2121

22+
import java.io.IOException;
2223
import java.nio.ByteBuffer;
2324
import java.util.ArrayList;
2425
import java.util.List;
@@ -42,7 +43,7 @@ public interface TileStore extends AutoCloseable {
4243
* @return the content of the tiles
4344
* @throws TileStoreException
4445
*/
45-
default List<ByteBuffer> read(List<TileCoord> tileCoords) throws TileStoreException {
46+
default List<ByteBuffer> read(List<TileCoord> tileCoords) throws TileStoreException, IOException {
4647
var blobs = new ArrayList<ByteBuffer>();
4748
for (var tileCoord : tileCoords) {
4849
blobs.add(read(tileCoord));

baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/utils/GeometryUtils.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.baremaps.openstreetmap.function.ProjectionTransformer;
2323
import org.locationtech.jts.geom.*;
24+
import org.locationtech.jts.geom.util.AffineTransformation;
2425
import org.locationtech.jts.io.ParseException;
2526
import org.locationtech.jts.io.WKBReader;
2627
import org.locationtech.jts.io.WKBWriter;
@@ -93,4 +94,25 @@ public static ProjectionTransformer projectionTransformer(int inputSRID, int out
9394
return new ProjectionTransformer(inputSRID, outputSRID);
9495
}
9596

97+
/**
98+
* Scales a geometry by a factor.
99+
*
100+
* @param geometry The geometry to scale
101+
* @param factor The factor to scale by
102+
* @return The scaled geometry
103+
*/
104+
public static Geometry scale(Geometry geometry, double factor) {
105+
AffineTransformation transform = AffineTransformation.scaleInstance(factor, factor);
106+
return transform.transform(geometry);
107+
}
108+
109+
public static Geometry createEnvelope(int xMin, int yMin, int xMax, int yMax) {
110+
return new GeometryFactory().createPolygon(new Coordinate[] {
111+
new Coordinate(xMin, yMin),
112+
new Coordinate(xMin, yMax),
113+
new Coordinate(xMax, yMax),
114+
new Coordinate(xMax, yMin),
115+
new Coordinate(xMin, yMin)
116+
});
117+
}
96118
}

0 commit comments

Comments
 (0)