Skip to content

Commit 4eb2d23

Browse files
committed
Compute hillshades and contour
1 parent ebc0f43 commit 4eb2d23

File tree

16 files changed

+472
-117
lines changed

16 files changed

+472
-117
lines changed

baremaps-cli/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ limitations under the License.
4343
<groupId>org.apache.baremaps</groupId>
4444
<artifactId>baremaps-core</artifactId>
4545
</dependency>
46+
<dependency>
47+
<groupId>org.apache.baremaps</groupId>
48+
<artifactId>baremaps-raster</artifactId>
49+
</dependency>
4650
<dependency>
4751
<groupId>org.apache.baremaps</groupId>
4852
<artifactId>baremaps-server</artifactId>

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

Lines changed: 7 additions & 7 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;
@@ -44,12 +44,12 @@
4444
description = "A toolkit for producing vector tiles.",
4545
versionProvider = VersionProvider.class,
4646
subcommands = {
47-
Workflow.class,
48-
Database.class,
49-
Map.class,
50-
Geocoder.class,
51-
IpLoc.class,
52-
Raster.class
47+
Workflow.class,
48+
Database.class,
49+
Map.class,
50+
Geocoder.class,
51+
IpLoc.class,
52+
Raster.class
5353
},
5454
sortOptions = false)
5555
@SuppressWarnings("squid:S106")
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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 static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
21+
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
22+
import static org.apache.baremaps.utils.ObjectMapperUtils.objectMapper;
23+
24+
import com.github.benmanes.caffeine.cache.Caffeine;
25+
import com.github.benmanes.caffeine.cache.LoadingCache;
26+
import com.linecorp.armeria.common.*;
27+
import com.linecorp.armeria.server.Server;
28+
import com.linecorp.armeria.server.annotation.Blocking;
29+
import com.linecorp.armeria.server.annotation.Get;
30+
import com.linecorp.armeria.server.annotation.JacksonResponseConverterFunction;
31+
import com.linecorp.armeria.server.annotation.Param;
32+
import com.linecorp.armeria.server.cors.CorsService;
33+
import com.linecorp.armeria.server.docs.DocService;
34+
import java.awt.*;
35+
import java.awt.image.BufferedImage;
36+
import java.io.ByteArrayOutputStream;
37+
import java.io.IOException;
38+
import java.net.URL;
39+
import java.nio.ByteBuffer;
40+
import java.util.concurrent.Callable;
41+
import java.util.function.Function;
42+
import java.util.function.Supplier;
43+
import javax.imageio.ImageIO;
44+
import org.apache.baremaps.raster.ImageUtils;
45+
import org.apache.baremaps.tilestore.TileCoord;
46+
import org.apache.baremaps.tilestore.TileStore;
47+
import org.apache.baremaps.tilestore.TileStoreException;
48+
import org.slf4j.Logger;
49+
import org.slf4j.LoggerFactory;
50+
import picocli.CommandLine.Command;
51+
import picocli.CommandLine.Option;
52+
53+
@Command(name = "hillshade", description = "Start a tile server that computes hillshades.")
54+
public class HillShade implements Callable<Integer> {
55+
56+
@Option(names = {"--host"}, paramLabel = "HOST", description = "The host of the server.")
57+
private String host = "localhost";
58+
59+
@Option(names = {"--port"}, paramLabel = "PORT", description = "The port of the server.")
60+
private int port = 9000;
61+
62+
@Override
63+
public Integer call() throws Exception {
64+
65+
var serverBuilder = Server.builder();
66+
serverBuilder.http(port);
67+
68+
var objectMapper = objectMapper();
69+
var jsonResponseConverter = new JacksonResponseConverterFunction(objectMapper);
70+
var tileStore = new HillShadeTileStore();
71+
72+
serverBuilder.annotatedService(new HillShadeTileResource(() -> tileStore),
73+
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)
126+
throws IOException {
127+
BufferedImage z1 =
128+
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() - 1, tileCoord.z()));
129+
BufferedImage z2 =
130+
provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() - 1, tileCoord.z()));
131+
BufferedImage z3 =
132+
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() - 1, tileCoord.z()));
133+
BufferedImage z4 =
134+
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y(), tileCoord.z()));
135+
BufferedImage z5 = provider.apply(tileCoord);
136+
BufferedImage z6 =
137+
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y(), tileCoord.z()));
138+
BufferedImage z7 =
139+
provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() + 1, tileCoord.z()));
140+
BufferedImage z8 =
141+
provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() + 1, tileCoord.z()));
142+
BufferedImage z9 =
143+
provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() + 1, tileCoord.z()));
144+
int kernelSize = z5.getWidth() * 3;
145+
BufferedImage kernel = new BufferedImage(kernelSize, kernelSize, z5.getType());
146+
for (int y = 0; y < z5.getHeight(); y++) {
147+
for (int x = 0; x < z5.getWidth(); x++) {
148+
kernel.setRGB(x, y, z1.getRGB(x, y));
149+
kernel.setRGB(x + z5.getWidth(), y, z2.getRGB(x, y));
150+
kernel.setRGB(x + 2 * z5.getWidth(), y, z3.getRGB(x, y));
151+
kernel.setRGB(x, y + z5.getHeight(), z4.getRGB(x, y));
152+
kernel.setRGB(x + z5.getWidth(), y + z5.getHeight(), z5.getRGB(x, y));
153+
kernel.setRGB(x + 2 * z5.getWidth(), y + z5.getHeight(), z6.getRGB(x, y));
154+
kernel.setRGB(x, y + 2 * z5.getHeight(), z7.getRGB(x, y));
155+
kernel.setRGB(x + z5.getWidth(), y + 2 * z5.getHeight(), z8.getRGB(x, y));
156+
kernel.setRGB(x + 2 * z5.getWidth(), y + 2 * z5.getHeight(), z9.getRGB(x, y));
157+
}
158+
}
159+
return kernel;
160+
}
161+
162+
@Override
163+
public ByteBuffer read(TileCoord tileCoord) throws TileStoreException {
164+
try {
165+
166+
var image = cache.get(tileCoord);
167+
var kernel = getKernel(tileCoord, cache::get);
168+
var buffer = kernel.getSubimage(
169+
image.getWidth() - 1,
170+
image.getHeight() - 1,
171+
image.getWidth() + 2,
172+
image.getHeight() + 2);
173+
174+
var grid = ImageUtils.grid(buffer);
175+
var hillshade = org.apache.baremaps.raster.hillshade.HillShade.hillShade(grid,
176+
buffer.getWidth(), buffer.getHeight(), 45, 315);
177+
178+
// Create an output image
179+
BufferedImage hillshadeImage =
180+
new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
181+
for (int y = 0; y < image.getHeight(); y++) {
182+
for (int x = 0; x < image.getWidth(); x++) {
183+
int value = (int) hillshade[(y + 1) * buffer.getHeight() + x + 1];
184+
hillshadeImage.setRGB(x, y, new Color(value, value, value).getRGB());
185+
}
186+
}
187+
188+
try (var baos = new ByteArrayOutputStream()) {
189+
ImageIO.write(hillshadeImage, "png", baos);
190+
baos.flush();
191+
return ByteBuffer.wrap(baos.toByteArray());
192+
}
193+
} catch (IOException e) {
194+
throw new RuntimeException(e);
195+
}
196+
}
197+
198+
@Override
199+
public void write(TileCoord tileCoord, ByteBuffer blob) throws TileStoreException {
200+
throw new UnsupportedOperationException();
201+
}
202+
203+
@Override
204+
public void delete(TileCoord tileCoord) throws TileStoreException {
205+
throw new UnsupportedOperationException();
206+
}
207+
208+
@Override
209+
public void close() throws Exception {
210+
// Do nothing
211+
}
212+
}
213+
214+
public class HillShadeTileResource {
215+
216+
private static final Logger logger =
217+
LoggerFactory.getLogger(org.apache.baremaps.server.TileResource.class);
218+
219+
// public static final String TILE_ENCODING = "gzip";
220+
221+
public static final String TILE_TYPE = "image/png";
222+
223+
private final Supplier<TileStore> tileStoreSupplier;
224+
225+
public HillShadeTileResource(Supplier<TileStore> tileStoreSupplier) {
226+
this.tileStoreSupplier = tileStoreSupplier;
227+
}
228+
229+
@Get("regex:^/tiles/(?<z>[0-9]+)/(?<x>[0-9]+)/(?<y>[0-9]+).png")
230+
@Blocking
231+
public HttpResponse tile(@Param("z") int z, @Param("x") int x, @Param("y") int y) {
232+
TileCoord tileCoord = new TileCoord(x, y, z);
233+
try {
234+
TileStore tileStore = tileStoreSupplier.get();
235+
ByteBuffer blob = tileStore.read(tileCoord);
236+
if (blob != null) {
237+
var headers = ResponseHeaders.builder(200)
238+
.add(CONTENT_TYPE, TILE_TYPE)
239+
// .add(CONTENT_ENCODING, TILE_ENCODING)
240+
.add(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
241+
.build();
242+
byte[] bytes = new byte[blob.remaining()];
243+
blob.get(bytes);
244+
HttpData data = HttpData.wrap(bytes);
245+
return HttpResponse.of(headers, data);
246+
} else {
247+
return HttpResponse.of(204);
248+
}
249+
} catch (TileStoreException ex) {
250+
logger.error("Error while reading tile.", ex);
251+
return HttpResponse.of(404);
252+
}
253+
}
254+
}
255+
}

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-cli/src/main/java/org/apache/baremaps/cli/rater/Hillshade.java

Lines changed: 0 additions & 40 deletions
This file was deleted.

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
@@ -191,6 +191,42 @@ public TileCoord parent() {
191191
return new TileCoord(x / 2, y / 2, z - 1);
192192
}
193193

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

0 commit comments

Comments
 (0)