Skip to content

Commit 38b70fe

Browse files
committed
Compute hillshades and contour
1 parent d30b08b commit 38b70fe

File tree

16 files changed

+472
-126
lines changed

16 files changed

+472
-126
lines changed

baremaps-cli/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ limitations under the License.
4747
<groupId>org.apache.baremaps</groupId>
4848
<artifactId>baremaps-server</artifactId>
4949
</dependency>
50+
<dependency>
51+
<groupId>org.apache.baremaps</groupId>
52+
<artifactId>baremaps-raster</artifactId>
53+
</dependency>
5054
<dependency>
5155
<groupId>org.apache.logging.log4j</groupId>
5256
<artifactId>log4j-api</artifactId>

baremaps-cli/src/main/java/org/apache/baremaps/cli/Baremaps.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.apache.baremaps.cli.geocoder.Geocoder;
2929
import org.apache.baremaps.cli.iploc.IpLoc;
3030
import org.apache.baremaps.cli.map.Map;
31-
import org.apache.baremaps.cli.rater.Raster;
31+
import org.apache.baremaps.cli.raster.Raster;
3232
import org.apache.baremaps.cli.workflow.Workflow;
3333
import org.apache.logging.log4j.Level;
3434
import org.apache.logging.log4j.core.config.Configurator;
@@ -45,7 +45,7 @@
4545
Geocoder.class,
4646
IpLoc.class,
4747
Raster.class
48-
},
48+
},
4949
sortOptions = false)
5050
public class Baremaps implements Callable<Integer> {
5151

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.baremaps.cli.raster;
19+
20+
import com.github.benmanes.caffeine.cache.Caffeine;
21+
import com.github.benmanes.caffeine.cache.LoadingCache;
22+
import com.linecorp.armeria.common.*;
23+
import com.linecorp.armeria.server.Server;
24+
import com.linecorp.armeria.server.annotation.Blocking;
25+
import com.linecorp.armeria.server.annotation.Get;
26+
import com.linecorp.armeria.server.annotation.JacksonResponseConverterFunction;
27+
import com.linecorp.armeria.server.annotation.Param;
28+
import com.linecorp.armeria.server.cors.CorsService;
29+
import com.linecorp.armeria.server.docs.DocService;
30+
import org.apache.baremaps.raster.ImageUtils;
31+
import org.apache.baremaps.tilestore.TileCoord;
32+
import org.apache.baremaps.tilestore.TileStore;
33+
import org.apache.baremaps.tilestore.TileStoreException;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
36+
import picocli.CommandLine.Command;
37+
import picocli.CommandLine.Option;
38+
39+
import javax.imageio.ImageIO;
40+
import java.awt.*;
41+
import java.awt.image.BufferedImage;
42+
import java.io.ByteArrayOutputStream;
43+
import java.io.IOException;
44+
import java.net.URL;
45+
import java.nio.ByteBuffer;
46+
import java.util.concurrent.Callable;
47+
import java.util.function.Function;
48+
import java.util.function.Supplier;
49+
50+
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
51+
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
52+
import static org.apache.baremaps.utils.ObjectMapperUtils.objectMapper;
53+
54+
@Command(name = "hillshade", description = "Start a tile server that computes hillshades.")
55+
public class HillShade implements Callable<Integer> {
56+
57+
@Option(names = {"--host"}, paramLabel = "HOST", description = "The host of the server.")
58+
private String host = "localhost";
59+
60+
@Option(names = {"--port"}, paramLabel = "PORT", description = "The port of the server.")
61+
private int port = 9000;
62+
63+
@Override
64+
public Integer call() throws Exception {
65+
66+
var serverBuilder = Server.builder();
67+
serverBuilder.http(port);
68+
69+
var objectMapper = objectMapper();
70+
var jsonResponseConverter = new JacksonResponseConverterFunction(objectMapper);
71+
var tileStore = new HillShadeTileStore();
72+
73+
serverBuilder.annotatedService(new HillShadeTileResource(() -> tileStore), jsonResponseConverter);
74+
75+
serverBuilder.decorator(CorsService.builderForAnyOrigin()
76+
.allowAllRequestHeaders(true)
77+
.allowRequestMethods(
78+
HttpMethod.GET,
79+
HttpMethod.POST,
80+
HttpMethod.PUT,
81+
HttpMethod.DELETE,
82+
HttpMethod.OPTIONS,
83+
HttpMethod.HEAD)
84+
.allowCredentials()
85+
.exposeHeaders(HttpHeaderNames.LOCATION)
86+
.newDecorator());
87+
88+
serverBuilder.serviceUnder("/docs", new DocService());
89+
90+
serverBuilder.disableServerHeader();
91+
serverBuilder.disableDateHeader();
92+
93+
var server = serverBuilder.build();
94+
95+
var startFuture = server.start();
96+
startFuture.join();
97+
98+
var shutdownFuture = server.closeOnJvmShutdown();
99+
shutdownFuture.join();
100+
101+
return 0;
102+
}
103+
104+
public static class HillShadeTileStore implements TileStore {
105+
106+
// private String url = "https://s3.amazonaws.com/elevation-tiles-prod/geotiff/{z}/{x}/{y}.tif";
107+
private String url = "https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png";
108+
109+
private final LoadingCache<TileCoord, BufferedImage> cache = Caffeine.newBuilder()
110+
.maximumSize(1000)
111+
.build(this::getImage);
112+
113+
public HillShadeTileStore() {
114+
// Default constructor
115+
}
116+
117+
public BufferedImage getImage(TileCoord tileCoord) throws IOException {
118+
var tileUrl = new URL(this.url
119+
.replace("{z}", String.valueOf(tileCoord.z()))
120+
.replace("{x}", String.valueOf(tileCoord.x()))
121+
.replace("{y}", String.valueOf(tileCoord.y())));
122+
return ImageIO.read(tileUrl);
123+
}
124+
125+
public BufferedImage getKernel(TileCoord tileCoord, Function<TileCoord, BufferedImage> provider) throws IOException {
126+
BufferedImage z1 = provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() - 1, tileCoord.z()));
127+
BufferedImage z2 = provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() - 1, tileCoord.z()));
128+
BufferedImage z3 = provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() - 1, tileCoord.z()));
129+
BufferedImage z4 = provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y(), tileCoord.z()));
130+
BufferedImage z5 = provider.apply(tileCoord);
131+
BufferedImage z6 = provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y(), tileCoord.z()));
132+
BufferedImage z7 = provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() + 1, tileCoord.z()));
133+
BufferedImage z8 = provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() + 1, tileCoord.z()));
134+
BufferedImage z9 = provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() + 1, tileCoord.z()));
135+
int kernelSize = z5.getWidth() * 3;
136+
BufferedImage kernel = new BufferedImage(kernelSize, kernelSize, z5.getType());
137+
for (int y = 0; y < z5.getHeight(); y++) {
138+
for (int x = 0; x < z5.getWidth(); x++) {
139+
kernel.setRGB(x, y, z1.getRGB(x, y));
140+
kernel.setRGB(x + z5.getWidth(), y, z2.getRGB(x, y));
141+
kernel.setRGB(x + 2 * z5.getWidth(), y, z3.getRGB(x, y));
142+
kernel.setRGB(x, y + z5.getHeight(), z4.getRGB(x, y));
143+
kernel.setRGB(x + z5.getWidth(), y + z5.getHeight(), z5.getRGB(x, y));
144+
kernel.setRGB(x + 2 * z5.getWidth(), y + z5.getHeight(), z6.getRGB(x, y));
145+
kernel.setRGB(x, y + 2 * z5.getHeight(), z7.getRGB(x, y));
146+
kernel.setRGB(x + z5.getWidth(), y + 2 * z5.getHeight(), z8.getRGB(x, y));
147+
kernel.setRGB(x + 2 * z5.getWidth(), y + 2 * z5.getHeight(), z9.getRGB(x, y));
148+
}
149+
}
150+
return kernel;
151+
}
152+
153+
@Override
154+
public ByteBuffer read(TileCoord tileCoord) throws TileStoreException {
155+
try {
156+
157+
var image = cache.get(tileCoord);
158+
var kernel = getKernel(tileCoord, cache::get);
159+
var buffer = kernel.getSubimage(
160+
image.getWidth() - 1,
161+
image.getHeight() - 1,
162+
image.getWidth() + 2,
163+
image.getHeight() + 2);
164+
165+
var grid = ImageUtils.grid(buffer);
166+
var hillshade = org.apache.baremaps.raster.hillshade.HillShade.hillShade(grid, buffer.getWidth(), buffer.getHeight(), 45, 315);
167+
168+
// Create an output image
169+
BufferedImage hillshadeImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
170+
for (int y = 0; y < image.getHeight(); y++) {
171+
for (int x = 0; x < image.getWidth(); x++) {
172+
int value = (int) hillshade[(y + 1) * buffer.getHeight() + x + 1];
173+
hillshadeImage.setRGB(x, y, new Color(value, value, value).getRGB());
174+
}
175+
}
176+
177+
try (var baos = new ByteArrayOutputStream()) {
178+
ImageIO.write(hillshadeImage, "png", baos);
179+
baos.flush();
180+
return ByteBuffer.wrap(baos.toByteArray());
181+
}
182+
} catch (IOException e) {
183+
throw new RuntimeException(e);
184+
}
185+
}
186+
187+
@Override
188+
public void write(TileCoord tileCoord, ByteBuffer blob) throws TileStoreException {
189+
throw new UnsupportedOperationException();
190+
}
191+
192+
@Override
193+
public void delete(TileCoord tileCoord) throws TileStoreException {
194+
throw new UnsupportedOperationException();
195+
}
196+
197+
@Override
198+
public void close() throws Exception {
199+
// Do nothing
200+
}
201+
}
202+
203+
public class HillShadeTileResource {
204+
205+
private static final Logger logger =
206+
LoggerFactory.getLogger(org.apache.baremaps.server.TileResource.class);
207+
208+
// public static final String TILE_ENCODING = "gzip";
209+
210+
public static final String TILE_TYPE = "image/png";
211+
212+
private final Supplier<TileStore> tileStoreSupplier;
213+
214+
public HillShadeTileResource(Supplier<TileStore> tileStoreSupplier) {
215+
this.tileStoreSupplier = tileStoreSupplier;
216+
}
217+
218+
@Get("regex:^/tiles/(?<z>[0-9]+)/(?<x>[0-9]+)/(?<y>[0-9]+).png")
219+
@Blocking
220+
public HttpResponse tile(@Param("z") int z, @Param("x") int x, @Param("y") int y) {
221+
TileCoord tileCoord = new TileCoord(x, y, z);
222+
try {
223+
TileStore tileStore = tileStoreSupplier.get();
224+
ByteBuffer blob = tileStore.read(tileCoord);
225+
if (blob != null) {
226+
var headers = ResponseHeaders.builder(200)
227+
.add(CONTENT_TYPE, TILE_TYPE)
228+
// .add(CONTENT_ENCODING, TILE_ENCODING)
229+
.add(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
230+
.build();
231+
byte[] bytes = new byte[blob.remaining()];
232+
blob.get(bytes);
233+
HttpData data = HttpData.wrap(bytes);
234+
return HttpResponse.of(headers, data);
235+
} else {
236+
return HttpResponse.of(204);
237+
}
238+
} catch (TileStoreException ex) {
239+
logger.error("Error while reading tile.", ex);
240+
return HttpResponse.of(404);
241+
}
242+
}
243+
}
244+
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.apache.baremaps.cli.rater;
18+
package org.apache.baremaps.cli.raster;
1919

2020

2121

22-
import org.apache.baremaps.cli.map.*;
2322
import picocli.CommandLine;
2423
import picocli.CommandLine.Command;
2524

2625
@Command(name = "raster", description = "Raster processing commands.",
27-
subcommands = {Hillshade.class},
26+
subcommands = {HillShade.class},
2827
sortOptions = false)
2928
public class Raster implements Runnable {
3029

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,42 @@ public TileCoord parent() {
190190
return new TileCoord(x / 2, y / 2, z - 1);
191191
}
192192

193+
/**
194+
* Returns the tile coordinate at the left of the current tile.
195+
*
196+
* @return the left tile
197+
*/
198+
public TileCoord left() {
199+
return new TileCoord(x - 1, y, z);
200+
}
201+
202+
/**
203+
* Returns the tile coordinate at the right of the current tile.
204+
*
205+
* @return the right tile
206+
*/
207+
public TileCoord right() {
208+
return new TileCoord(x + 1, y, z);
209+
}
210+
211+
/**
212+
* Returns the tile coordinate at the top of the current tile.
213+
*
214+
* @return the top tile
215+
*/
216+
public TileCoord top() {
217+
return new TileCoord(x, y - 1, z);
218+
}
219+
220+
/**
221+
* Returns the tile coordinate at the bottom of the current tile.
222+
*
223+
* @return the bottom tile
224+
*/
225+
public TileCoord bottom() {
226+
return new TileCoord(x, y + 1, z);
227+
}
228+
193229
/**
194230
* Returns the envelope of the tile coordinate.
195231
*

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,30 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.apache.baremaps.cli.rater;
18+
package org.apache.baremaps.raster;
1919

20-
import java.util.concurrent.Callable;
21-
import picocli.CommandLine.Command;
22-
import picocli.CommandLine.Option;
20+
import java.awt.image.BufferedImage;
2321

24-
@Command(name = "hillshade", description = "Start a tile server that computes hillshades.")
25-
public class Hillshade implements Callable<Integer> {
22+
public class ImageUtils {
2623

27-
@Option(names = {"--host"}, paramLabel = "HOST", description = "The host of the server.")
28-
private String host = "localhost";
2924

30-
@Option(names = {"--port"}, paramLabel = "PORT", description = "The port of the server.")
31-
private int port = 9000;
25+
public static double[] grid(BufferedImage image) {
26+
int gridSize = image.getWidth();
27+
double[] terrain = new double[gridSize * gridSize];
3228

33-
@Override
34-
public Integer call() throws Exception {
29+
int tileSize = image.getWidth();
3530

31+
// decode terrain values
32+
for (int y = 0; y < tileSize; y++) {
33+
for (int x = 0; x < tileSize; x++) {
34+
int r = (image.getRGB(x, y) >> 16) & 0xFF;
35+
int g = (image.getRGB(x, y) >> 8) & 0xFF;
36+
int b = image.getRGB(x, y) & 0xFF;
37+
terrain[y * gridSize + x] = (r * 256.0 * 256.0 + g * 256.0 + b) / 10.0 - 10000.0;
38+
}
39+
}
3640

41+
return terrain;
42+
}
3743

38-
return 0;
39-
}
4044
}

baremaps-raster/src/main/java/org/apache/baremaps/raster/contour/IsoLines.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ public static List<IsoLine> isoLines(double[] grid, int gridSize, double level)
8989
return isoLines;
9090
}
9191

92+
public static List<IsoLine> isoLines(double[] grid, int gridSize, int start, int end, int interval) {
93+
List<IsoLine> isoLines = new ArrayList<>();
94+
for (int level = start; level < end; level++) {
95+
isoLines.addAll(isoLines(grid, gridSize, level));
96+
}
97+
return isoLines;
98+
}
99+
92100
private static Point interpolate(
93101
int x1,
94102
int y1,

0 commit comments

Comments
 (0)