Skip to content

Commit c818573

Browse files
committed
Improve API
1 parent ba21a24 commit c818573

File tree

7 files changed

+157
-37
lines changed

7 files changed

+157
-37
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.util.function.Supplier;
4343
import javax.imageio.ImageIO;
4444
import org.apache.baremaps.raster.elevation.ElevationUtils;
45+
import org.apache.baremaps.raster.elevation.HillshadeCalculator;
4546
import org.apache.baremaps.tilestore.TileCoord;
4647
import org.apache.baremaps.tilestore.TileStore;
4748
import org.apache.baremaps.tilestore.TileStoreException;
@@ -173,8 +174,9 @@ public ByteBuffer read(TileCoord tileCoord) throws TileStoreException {
173174
image.getHeight() + 2);
174175

175176
var grid = ElevationUtils.imageToGrid(buffer);
176-
var hillshade = org.apache.baremaps.raster.elevation.HillShade.hillShade(grid,
177-
buffer.getWidth(), buffer.getHeight(), 45, 315);
177+
var hillshade =
178+
new HillshadeCalculator(grid, buffer.getWidth(), buffer.getHeight(), 1, true)
179+
.calculate(45, 315);
178180

179181
// Create an output image
180182
BufferedImage hillshadeImage =
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.raster.elevation;
19+
20+
import java.util.Arrays;
21+
import org.locationtech.jts.geom.Coordinate;
22+
23+
public class ChaikinSmoother {
24+
25+
private final Coordinate[] coordinates;
26+
private final double minX;
27+
private final double minY;
28+
private final double maxX;
29+
private final double maxY;
30+
private final boolean isOpen;
31+
32+
public ChaikinSmoother(Coordinate[] coordinates, double minX, double minY, double maxX,
33+
double maxY) {
34+
this.coordinates = Arrays.copyOf(coordinates, coordinates.length);
35+
this.minX = minX;
36+
this.minY = minY;
37+
this.maxX = maxX;
38+
this.maxY = maxY;
39+
this.isOpen = !coordinates[0].equals(coordinates[coordinates.length - 1]);
40+
}
41+
42+
public Coordinate[] smooth(int iterations, double factor) {
43+
Coordinate[] result = isOpen
44+
? Arrays.copyOf(coordinates, coordinates.length - 1)
45+
: coordinates;
46+
47+
double f1 = 1 - factor;
48+
double f2 = factor;
49+
50+
// Apply the algorithm repeatedly
51+
for (int n = 0; n < iterations; n++) {
52+
Coordinate[] temp = new Coordinate[isOpen ? 2 * result.length - 2 : 2 * result.length];
53+
54+
for (int i = 0; i < result.length; i++) {
55+
if (isOnBoundary(result[i]) || isOnBoundary(result[(i + 1) % result.length])) {
56+
temp[2 * i] = result[i];
57+
temp[2 * i + 1] = result[(i + 1) % result.length];
58+
} else {
59+
temp[2 * i] = new Coordinate(
60+
f1 * result[i].x + f2 * result[(i + 1) % result.length].x,
61+
f1 * result[i].y + f2 * result[(i + 1) % result.length].y);
62+
temp[2 * i + 1] = new Coordinate(
63+
f2 * result[i].x + f1 * result[(i + 1) % result.length].x,
64+
f2 * result[i].y + f1 * result[(i + 1) % result.length].y);
65+
}
66+
}
67+
68+
if (isOpen) {
69+
temp[0] = result[0];
70+
temp[temp.length - 1] = result[result.length - 1];
71+
}
72+
73+
result = temp;
74+
}
75+
76+
if (!isOpen) {
77+
result = Arrays.copyOf(result, result.length + 1);
78+
result[result.length - 1] = result[0];
79+
}
80+
81+
return result;
82+
}
83+
84+
private boolean isOnBoundary(Coordinate coord) {
85+
return coord.x == minX || coord.x == maxX || coord.y == minY || coord.y == maxY;
86+
}
87+
}

baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ContourTracer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.baremaps.raster.elevation;
1919

2020
import java.util.ArrayList;
21+
import java.util.Arrays;
2122
import java.util.List;
2223
import org.locationtech.jts.geom.*;
2324
import org.locationtech.jts.geom.util.GeometryTransformer;
@@ -55,7 +56,7 @@ public class ContourTracer {
5556
*/
5657
public ContourTracer(double[] grid, int width, int height, boolean normalize,
5758
boolean polygonize) {
58-
this.grid = grid;
59+
this.grid = Arrays.copyOf(grid, grid.length);
5960
this.width = width;
6061
this.height = height;
6162
this.normalize = normalize;

baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/HillShade.java renamed to baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/HillshadeCalculator.java

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,64 +20,62 @@
2020
/**
2121
* Provides methods for generating hillshade effects on digital elevation models (DEMs).
2222
*/
23-
public class HillShade {
23+
public class HillshadeCalculator {
2424

2525
private static final double DEFAULT_SCALE = 0.1;
2626
private static final double ENHANCED_SCALE = 1.0;
2727
private static final double MIN_REFLECTANCE = 0.0;
2828
private static final double MAX_REFLECTANCE = 255.0;
2929
private static final double TWO_PI = 2 * Math.PI;
3030

31-
private HillShade() {
32-
// Prevent instantiation
31+
private final double[] dem;
32+
private final int width;
33+
private final int height;
34+
private final double scale;
35+
private final boolean isSimple;
36+
37+
public HillshadeCalculator(double[] dem, int width, int height, double scale, boolean isSimple) {
38+
this.dem = dem;
39+
this.width = width;
40+
this.height = height;
41+
this.scale = scale;
42+
this.isSimple = isSimple;
3343
}
3444

3545
/**
36-
* Generates a hillshade effect on the given DEM.
46+
* Generates a hillshade effect on the DEM.
3747
*
38-
* @param dem The digital elevation model data
39-
* @param width The width of the DEM
40-
* @param height The height of the DEM
4148
* @param sunAltitude The sun's altitude in degrees
4249
* @param sunAzimuth The sun's azimuth in degrees
4350
* @return An array representing the hillshade effect
4451
*/
45-
public static double[] hillShade(double[] dem, int width, int height, double sunAltitude,
46-
double sunAzimuth) {
52+
public double[] calculate(double sunAltitude, double sunAzimuth) {
4753
validateInput(dem, width, height, sunAltitude, sunAzimuth);
48-
return calculateHillShade(dem, width, height, sunAltitude, sunAzimuth, DEFAULT_SCALE, true);
54+
return calculateHillshade(sunAltitude, sunAzimuth);
4955
}
5056

5157
/**
5258
* Generates a hillshade effect on the given DEM.
5359
*
54-
* @param dem The digital elevation model data
55-
* @param width The width of the DEM
56-
* @param height The height of the DEM
5760
* @param sunAltitude The sun's altitude in degrees
5861
* @param sunAzimuth The sun's azimuth in degrees
5962
* @return An array representing the hillshade effect
6063
*/
61-
public static double[] hillShade(double[] dem, int width, int height, double sunAltitude,
62-
double sunAzimuth, double scale, boolean isSimple) {
64+
public double[] calculate(double sunAltitude, double sunAzimuth, double scale, boolean isSimple) {
6365
validateInput(dem, width, height, sunAltitude, sunAzimuth);
64-
return calculateHillShade(dem, width, height, sunAltitude, sunAzimuth, scale, isSimple);
66+
return calculateHillshade(sunAltitude, sunAzimuth);
6567
}
6668

6769
/**
6870
* Generates an enhanced hillshade effect on the given DEM.
6971
*
70-
* @param dem The digital elevation model data
71-
* @param width The width of the DEM
72-
* @param height The height of the DEM
7372
* @param sunAltitude The sun's altitude in degrees
7473
* @param sunAzimuth The sun's azimuth in degrees
7574
* @return An array representing the enhanced hillshade effect
7675
*/
77-
public static double[] hillShadeEnhanced(double[] dem, int width, int height, double sunAltitude,
78-
double sunAzimuth) {
76+
public double[] calculateEnhanced(double sunAltitude, double sunAzimuth) {
7977
validateInput(dem, width, height, sunAltitude, sunAzimuth);
80-
return calculateHillShade(dem, width, height, sunAltitude, sunAzimuth, ENHANCED_SCALE, false);
78+
return calculateHillshade(sunAltitude, sunAzimuth);
8179
}
8280

8381
private static void validateInput(double[] dem, int width, int height, double sunAltitude,
@@ -99,12 +97,11 @@ private static void validateInput(double[] dem, int width, int height, double su
9997
}
10098
}
10199

102-
private static double[] calculateHillShade(double[] dem, int width, int height,
103-
double sunAltitude, double sunAzimuth, double scale, boolean isSimple) {
100+
private double[] calculateHillshade(double sunAltitude, double sunAzimuth) {
104101
double[] hillshade = new double[dem.length];
105102

106103
double sunAltitudeRad = Math.toRadians(sunAltitude);
107-
double sunAzimuthRad = Math.toRadians(sunAzimuth + (isSimple ? 90 : 0));
104+
double sunAzimuthRad = Math.toRadians(sunAzimuth + (isSimple ? 90 : 180));
108105
double cosSunAltitude = Math.cos(sunAltitudeRad);
109106
double sinSunAltitude = Math.sin(sunAltitudeRad);
110107

baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ContourRenderer.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import java.util.stream.Stream;
2626
import javax.imageio.ImageIO;
2727
import javax.swing.*;
28+
import org.locationtech.jts.geom.Coordinate;
2829
import org.locationtech.jts.geom.Geometry;
30+
import org.locationtech.jts.geom.GeometryFactory;
2931

3032
public class ContourRenderer {
3133

@@ -37,13 +39,38 @@ public static void main(String[] args) throws IOException {
3739
.toAbsolutePath().toFile();
3840
var image = ImageIO.read(path);
3941

42+
// Downscale the image by a factor of 16
43+
image = resizeImage(image, 32, 32);
44+
4045
// Convert the image to a grid
4146
double[] grid = ElevationUtils.imageToGrid(image);
4247

4348
List<Geometry> contour =
4449
new ContourTracer(grid, image.getWidth(), image.getHeight(), true, true)
4550
.traceContours(0, 9000, 100);
4651

52+
// Scale the image back to its original size
53+
image = resizeImage(image, image.getWidth() * 16, image.getHeight() * 16);
54+
55+
// Scale the contour back to its original size
56+
contour = contour.stream()
57+
.map(polygon -> {
58+
var coordinates = Stream.of(polygon.getCoordinates())
59+
.map(c -> new Coordinate(c.getX() * 16, c.getY() * 16))
60+
.toArray(Coordinate[]::new);
61+
return (Geometry) new GeometryFactory().createPolygon(coordinates);
62+
})
63+
.toList();
64+
65+
// Smooth the contour with the Chaikin algorithm
66+
contour = contour.stream()
67+
.map(polygon -> {
68+
var coordinates =
69+
new ChaikinSmoother(polygon.getCoordinates(), 0, 0, 512, 512).smooth(2, 0.25);
70+
return (Geometry) new GeometryFactory().createPolygon(coordinates);
71+
})
72+
.toList();
73+
4774
// Create a frame to display the contours
4875
JFrame frame = new JFrame("Contour Lines");
4976
frame.setSize(image.getWidth() + 20, image.getHeight() + 20);

baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeRenderer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ private void redrawHillshade() {
129129
double scale = scaleSlider.getValue() / 10.0;
130130
boolean isSimple = isSimpleCheckbox.isSelected();
131131

132-
double[] hillshade = HillShade.hillShade(grid, originalImage.getWidth(),
133-
originalImage.getHeight(), sunAltitude, sunAzimuth, scale, isSimple);
132+
double[] hillshade = new HillshadeCalculator(grid, originalImage.getWidth(),
133+
originalImage.getHeight(), scale, isSimple).calculate(sunAltitude, sunAzimuth);
134134

135135
BufferedImage hillshadeImage = new BufferedImage(originalImage.getWidth(),
136136
originalImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY);

baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeTest.java renamed to baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillshadeCalculatorTest.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import org.junit.jupiter.params.provider.Arguments;
2727
import org.junit.jupiter.params.provider.MethodSource;
2828

29-
class HillShadeTest {
29+
class HillshadeCalculatorTest {
3030

3131
private static final double DELTA = 1e-6;
3232

@@ -43,7 +43,8 @@ void testHillShadeValidInput() {
4343
double sunAltitude = 45;
4444
double sunAzimuth = 315;
4545

46-
double[] result = HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth);
46+
double[] result =
47+
new HillshadeCalculator(dem, width, height, 1, true).calculate(sunAltitude, sunAzimuth);
4748

4849
assertNotNull(result);
4950
assertEquals(dem.length, result.length);
@@ -62,7 +63,8 @@ void testHillShadeEnhancedValidInput() {
6263
double sunAltitude = 45;
6364
double sunAzimuth = 315;
6465

65-
double[] result = HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth);
66+
double[] result =
67+
new HillshadeCalculator(dem, width, height, 1, true).calculate(sunAltitude, sunAzimuth);
6668

6769
assertNotNull(result);
6870
assertEquals(dem.length, result.length);
@@ -74,7 +76,8 @@ void testHillShadeEnhancedValidInput() {
7476
void testHillShadeInvalidInput(double[] dem, int width, int height, double sunAltitude,
7577
double sunAzimuth, Class<? extends Exception> expectedException) {
7678
assertThrows(expectedException,
77-
() -> HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth));
79+
() -> new HillshadeCalculator(dem, width, height, 1, true).calculate(sunAltitude,
80+
sunAzimuth));
7881
}
7982

8083
@ParameterizedTest
@@ -83,7 +86,8 @@ void testHillShadeInvalidInput(double[] dem, int width, int height, double sunAl
8386
void testHillShadeEnhancedInvalidInput(double[] dem, int width, int height, double sunAltitude,
8487
double sunAzimuth, Class<? extends Exception> expectedException) {
8588
assertThrows(expectedException,
86-
() -> HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth));
89+
() -> new HillshadeCalculator(dem, width, height, 1, true).calculate(sunAltitude,
90+
sunAzimuth));
8791
}
8892

8993
private static Stream<Arguments> provideInvalidInput() {
@@ -111,7 +115,8 @@ void testHillShadeOutputRange() {
111115
double sunAltitude = 45;
112116
double sunAzimuth = 315;
113117

114-
double[] result = HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth);
118+
double[] result =
119+
new HillshadeCalculator(dem, width, height, 1, true).calculate(sunAltitude, sunAzimuth);
115120

116121
for (double value : result) {
117122
assertTrue(value >= 0 && value <= 255, "Hillshade value should be between 0 and 255");
@@ -130,7 +135,8 @@ void testHillShadeEnhancedOutputRange() {
130135
double sunAltitude = 45;
131136
double sunAzimuth = 315;
132137

133-
double[] result = HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth);
138+
double[] result =
139+
new HillshadeCalculator(dem, width, height, 1, true).calculate(sunAltitude, sunAzimuth);
134140

135141
for (double value : result) {
136142
assertTrue(value >= 0 && value <= 255, "Hillshade value should be between 0 and 255");

0 commit comments

Comments
 (0)