diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java index 7defd18ce..4125e6716 100644 --- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java +++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java @@ -41,7 +41,7 @@ import java.util.function.Function; import java.util.function.Supplier; import javax.imageio.ImageIO; -import org.apache.baremaps.raster.ImageUtils; +import org.apache.baremaps.raster.ElevationUtils; import org.apache.baremaps.tilestore.TileCoord; import org.apache.baremaps.tilestore.TileStore; import org.apache.baremaps.tilestore.TileStoreException; @@ -104,7 +104,8 @@ public Integer call() throws Exception { public static class HillShadeTileStore implements TileStore { // private String url = "https://s3.amazonaws.com/elevation-tiles-prod/geotiff/{z}/{x}/{y}.tif"; - private String url = "https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png"; + // private String url = "https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png"; + private String url = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"; private final LoadingCache cache = Caffeine.newBuilder() .maximumSize(1000) @@ -171,8 +172,8 @@ public ByteBuffer read(TileCoord tileCoord) throws TileStoreException { image.getWidth() + 2, image.getHeight() + 2); - var grid = ImageUtils.grid(buffer); - var hillshade = org.apache.baremaps.raster.hillshade.HillShade.hillShade(grid, + var grid = ElevationUtils.imageToGrid(buffer); + var hillshade = org.apache.baremaps.raster.elevation.HillShade.hillShade(grid, buffer.getWidth(), buffer.getHeight(), 45, 315); // Create an output image diff --git a/baremaps-raster/pom.xml b/baremaps-raster/pom.xml index 6499ffd50..6ee64ad58 100644 --- a/baremaps-raster/pom.xml +++ b/baremaps-raster/pom.xml @@ -11,7 +11,10 @@ com.twelvemonkeys.imageio imageio-tiff - 3.11.0 + + + org.locationtech.jts + jts-core diff --git a/baremaps-raster/src/main/java/org/apache/baremaps/raster/ElevationUtils.java b/baremaps-raster/src/main/java/org/apache/baremaps/raster/ElevationUtils.java new file mode 100644 index 000000000..f5c7c28cd --- /dev/null +++ b/baremaps-raster/src/main/java/org/apache/baremaps/raster/ElevationUtils.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.raster; + +import java.awt.image.BufferedImage; + +/** + * Provides utility methods for processing raster images, particularly for elevation data. + */ +public class ElevationUtils { + + private static final double ELEVATION_SCALE = 256.0 * 256.0; + private static final double ELEVATION_OFFSET = 10000.0; + + private ElevationUtils() { + // Private constructor to prevent instantiation + } + + /** + * Converts a BufferedImage to a grid of elevation values. + * + * @param image The input BufferedImage + * @return A double array representing the elevation grid + */ + public static double[] imageToGrid(BufferedImage image) { + validateImage(image); + int width = image.getWidth(); + int height = image.getHeight(); + double[] grid = new double[width * height]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + grid[y * width + x] = pixelToElevation(image.getRGB(x, y)); + } + } + + return grid; + } + + /** + * Converts a grid of elevation values to a BufferedImage. + * + * @param grid The input elevation grid + * @param width The width of the grid + * @param height The height of the grid + * @return A BufferedImage representing the elevation data + */ + public static BufferedImage gridToImage(double[] grid, int width, int height) { + validateGrid(grid, width, height); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + image.setRGB(x, y, elevationToPixel(grid[y * width + x])); + } + } + + return image; + } + + private static double pixelToElevation(int rgb) { + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + return (r * ELEVATION_SCALE + g * 256.0 + b) / 10.0 - ELEVATION_OFFSET; + } + + private static int elevationToPixel(double elevation) { + int value = (int) ((elevation + ELEVATION_OFFSET) * 10.0); + int r = (value >> 16) & 0xFF; + int g = (value >> 8) & 0xFF; + int b = value & 0xFF; + return (r << 16) | (g << 8) | b; + } + + private static void validateImage(BufferedImage image) { + if (image == null) { + throw new IllegalArgumentException("Input image cannot be null"); + } + if (image.getWidth() <= 0 || image.getHeight() <= 0) { + throw new IllegalArgumentException("Image dimensions must be positive"); + } + } + + private static void validateGrid(double[] grid, int width, int height) { + if (grid == null || grid.length == 0) { + throw new IllegalArgumentException("Grid array cannot be null or empty"); + } + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Width and height must be positive"); + } + if (grid.length != width * height) { + throw new IllegalArgumentException("Grid array length does not match width * height"); + } + } + +} diff --git a/baremaps-raster/src/main/java/org/apache/baremaps/raster/ImageUtils.java b/baremaps-raster/src/main/java/org/apache/baremaps/raster/ImageUtils.java deleted file mode 100644 index 854ff5c16..000000000 --- a/baremaps-raster/src/main/java/org/apache/baremaps/raster/ImageUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.baremaps.raster; - -import java.awt.image.BufferedImage; - -public class ImageUtils { - - - public static double[] grid(BufferedImage image) { - int gridSize = image.getWidth(); - double[] terrain = new double[gridSize * gridSize]; - - int tileSize = image.getWidth(); - - // decode terrain values - for (int y = 0; y < tileSize; y++) { - for (int x = 0; x < tileSize; x++) { - int r = (image.getRGB(x, y) >> 16) & 0xFF; - int g = (image.getRGB(x, y) >> 8) & 0xFF; - int b = image.getRGB(x, y) & 0xFF; - terrain[y * gridSize + x] = (r * 256.0 * 256.0 + g * 256.0 + b) / 10.0 - 10000.0; - } - } - - return terrain; - } - -} diff --git a/baremaps-raster/src/main/java/org/apache/baremaps/raster/contour/IsoLines.java b/baremaps-raster/src/main/java/org/apache/baremaps/raster/contour/IsoLines.java deleted file mode 100644 index e2f7ba895..000000000 --- a/baremaps-raster/src/main/java/org/apache/baremaps/raster/contour/IsoLines.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.baremaps.raster.contour; - -import java.util.ArrayList; -import java.util.List; - -public class IsoLines { - - public record Point(double x, double y) { - } - - public record IsoLine(List points) { - } - - public static List isoLines(double[] grid, int gridSize, double level) { - List isoLines = new ArrayList<>(); - for (int y = 0; y < gridSize - 1; y++) { - for (int x = 0; x < gridSize - 1; x++) { - int index = (grid[y * (gridSize + 1) + x] > level ? 1 : 0) | - (grid[y * (gridSize + 1) + (x + 1)] > level ? 2 : 0) | - (grid[(y + 1) * (gridSize + 1) + (x + 1)] > level ? 4 : 0) | - (grid[(y + 1) * (gridSize + 1) + x] > level ? 8 : 0); - List points = new ArrayList<>(); - switch (index) { - case 1: - case 14: - points.add(interpolate(x, y, x, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x, y, x + 1, y, grid, gridSize + 1, level)); - break; - case 2: - case 13: - points.add(interpolate(x + 1, y, x, y, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y, x + 1, y + 1, grid, gridSize + 1, level)); - break; - case 3: - case 12: - points.add(interpolate(x, y, x, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y, x + 1, y + 1, grid, gridSize + 1, level)); - break; - case 4: - case 11: - points.add(interpolate(x + 1, y + 1, x + 1, y, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y + 1, x, y + 1, grid, gridSize + 1, level)); - break; - case 5: - points.add(interpolate(x, y, x, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x, y + 1, x + 1, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y, x + 1, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y, x, y, grid, gridSize + 1, level)); - break; - case 6: - case 9: - points.add(interpolate(x, y, x + 1, y, grid, gridSize + 1, level)); - points.add(interpolate(x, y + 1, x + 1, y + 1, grid, gridSize + 1, level)); - break; - case 7: - case 8: - points.add(interpolate(x, y, x, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x, y + 1, x + 1, y + 1, grid, gridSize + 1, level)); - break; - case 10: - points.add(interpolate(x, y, x + 1, y, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y, x + 1, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x + 1, y + 1, x, y + 1, grid, gridSize + 1, level)); - points.add(interpolate(x, y + 1, x, y, grid, gridSize + 1, level)); - break; - } - if (!points.isEmpty()) { - isoLines.add(new IsoLine(points)); - } - } - } - return isoLines; - } - - public static List isoLines(double[] grid, int gridSize, int start, int end, - int interval) { - List isoLines = new ArrayList<>(); - for (int level = start; level < end; level++) { - isoLines.addAll(isoLines(grid, gridSize, level)); - } - return isoLines; - } - - private static Point interpolate( - int x1, - int y1, - int x2, - int y2, - double[] grid, - int width, - double level) { - double v1 = grid[y1 * width + x1]; - double v2 = grid[y2 * width + x2]; - double t = (level - v1) / (v2 - v1); - return new Point(x1 + t * (x2 - x1), y1 + t * (y2 - y1)); - } - -} diff --git a/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/HillShade.java b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/HillShade.java new file mode 100644 index 000000000..5c92c264a --- /dev/null +++ b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/HillShade.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.raster.elevation; + +/** + * Provides methods for generating hillshade effects on digital elevation models (DEMs). + */ +public class HillShade { + + private static final double DEFAULT_SCALE = 0.1; + private static final double ENHANCED_SCALE = 1.0; + private static final double MIN_REFLECTANCE = 0.0; + private static final double MAX_REFLECTANCE = 255.0; + private static final double TWO_PI = 2 * Math.PI; + + private HillShade() { + // Prevent instantiation + } + + /** + * Generates a hillshade effect on the given DEM. + * + * @param dem The digital elevation model data + * @param width The width of the DEM + * @param height The height of the DEM + * @param sunAltitude The sun's altitude in degrees + * @param sunAzimuth The sun's azimuth in degrees + * @return An array representing the hillshade effect + */ + public static double[] hillShade(double[] dem, int width, int height, double sunAltitude, + double sunAzimuth) { + validateInput(dem, width, height, sunAltitude, sunAzimuth); + return calculateHillShade(dem, width, height, sunAltitude, sunAzimuth, DEFAULT_SCALE, true); + } + + /** + * Generates a hillshade effect on the given DEM. + * + * @param dem The digital elevation model data + * @param width The width of the DEM + * @param height The height of the DEM + * @param sunAltitude The sun's altitude in degrees + * @param sunAzimuth The sun's azimuth in degrees + * @return An array representing the hillshade effect + */ + public static double[] hillShade(double[] dem, int width, int height, double sunAltitude, + double sunAzimuth, double scale, boolean isSimple) { + validateInput(dem, width, height, sunAltitude, sunAzimuth); + return calculateHillShade(dem, width, height, sunAltitude, sunAzimuth, scale, isSimple); + } + + /** + * Generates an enhanced hillshade effect on the given DEM. + * + * @param dem The digital elevation model data + * @param width The width of the DEM + * @param height The height of the DEM + * @param sunAltitude The sun's altitude in degrees + * @param sunAzimuth The sun's azimuth in degrees + * @return An array representing the enhanced hillshade effect + */ + public static double[] hillShadeEnhanced(double[] dem, int width, int height, double sunAltitude, + double sunAzimuth) { + validateInput(dem, width, height, sunAltitude, sunAzimuth); + return calculateHillShade(dem, width, height, sunAltitude, sunAzimuth, ENHANCED_SCALE, false); + } + + private static void validateInput(double[] dem, int width, int height, double sunAltitude, + double sunAzimuth) { + if (dem == null || dem.length == 0) { + throw new IllegalArgumentException("DEM array cannot be null or empty"); + } + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Width and height must be positive"); + } + if (dem.length != width * height) { + throw new IllegalArgumentException("DEM array length does not match width * height"); + } + if (sunAltitude < 0 || sunAltitude > 90) { + throw new IllegalArgumentException("Sun altitude must be between 0 and 90 degrees"); + } + if (sunAzimuth < 0 || sunAzimuth > 360) { + throw new IllegalArgumentException("Sun azimuth must be between 0 and 360 degrees"); + } + } + + private static double[] calculateHillShade(double[] dem, int width, int height, + double sunAltitude, double sunAzimuth, double scale, boolean isSimple) { + double[] hillshade = new double[dem.length]; + + double sunAltitudeRad = Math.toRadians(sunAltitude); + double sunAzimuthRad = Math.toRadians(sunAzimuth + (isSimple ? 90 : 0)); + double cosSunAltitude = Math.cos(sunAltitudeRad); + double sinSunAltitude = Math.sin(sunAltitudeRad); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int top = Math.max(y - 1, 0); + int bottom = Math.min(y + 1, height - 1); + int left = Math.max(x - 1, 0); + int right = Math.min(x + 1, width - 1); + + double dzdx, dzdy; + if (isSimple) { + dzdx = (dem[y * width + right] - dem[y * width + left]) / 2.0; + dzdy = (dem[bottom * width + x] - dem[top * width + x]) / 2.0; + } else { + dzdx = ((dem[top * width + right] + 2 * dem[y * width + right] + + dem[bottom * width + right]) - + (dem[top * width + left] + 2 * dem[y * width + left] + dem[bottom * width + left])) + / 8.0; + dzdy = ((dem[bottom * width + left] + 2 * dem[bottom * width + x] + + dem[bottom * width + right]) - + (dem[top * width + left] + 2 * dem[top * width + x] + dem[top * width + right])) + / 8.0; + } + + double slope = Math.atan(scale * Math.hypot(dzdx, dzdy)); + double aspect = Math.atan2(dzdy, isSimple ? dzdx : -dzdx); + if (aspect < 0) { + aspect += TWO_PI; + } + + double reflectance = cosSunAltitude * Math.cos(slope) + + sinSunAltitude * Math.sin(slope) * Math.cos(sunAzimuthRad - aspect); + + hillshade[y * width + x] = + Math.max(MIN_REFLECTANCE, Math.min(MAX_REFLECTANCE, reflectance * MAX_REFLECTANCE)); + } + } + + return hillshade; + } + +} diff --git a/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/IsoLines.java b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/IsoLines.java new file mode 100644 index 000000000..384c41cbf --- /dev/null +++ b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/IsoLines.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.raster.elevation; + +import java.util.ArrayList; +import java.util.List; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.operation.linemerge.LineMerger; + +/** + * Provides methods for generating isoline contours from digital elevation models (DEMs). + */ +public class IsoLines { + + private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); + + private static final double EPSILON = 1e-10; + + private IsoLines() { + // Prevent instantiation + } + + /** + * Generates isolines for a given grid at a specific level. + * + * @param grid The elevation data + * @param width The width of the grid + * @param height The height of the grid + * @param level The elevation level for which to generate isolines + * @param normalize Whether to normalize the coordinates + * @return A list of LineString objects representing the isolines + */ + public static List generateIsoLines( + double[] grid, int width, int height, + double level, boolean normalize) { + validateInput(grid, width, height); + List lineStrings = new ArrayList<>(); + for (int y = 0; y < height - 1; y++) { + for (int x = 0; x < width - 1; x++) { + processCell(grid, width, height, level, normalize, lineStrings, y, x); + } + } + return mergeLineStrings(lineStrings); + } + + /** + * Generates isolines for a given grid at multiple levels within a specified range. + * + * @param grid The elevation data + * @param width The width of the grid + * @param height The height of the grid + * @param start The starting elevation level + * @param end The ending elevation level + * @param interval The interval between elevation levels + * @param normalize Whether to normalize the coordinates + * @return A list of LineString objects representing the isolines + */ + public static List generateIsoLines( + double[] grid, int width, int height, + int start, int end, int interval, + boolean normalize) { + validateInput(grid, width, height); + List isoLines = new ArrayList<>(); + for (int level = start; level < end; level += interval) { + isoLines.addAll(generateIsoLines(grid, width, height, level, normalize)); + } + return isoLines; + } + + private static List mergeLineStrings(List lineStrings) { + LineMerger lineMerger = new LineMerger(); + lineMerger.add(lineStrings); + return new ArrayList<>(lineMerger.getMergedLineStrings()); + } + + private static void validateInput(double[] grid, int width, int height) { + if (grid == null || grid.length == 0) { + throw new IllegalArgumentException("Grid array cannot be null or empty"); + } + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Width and height must be positive"); + } + if (grid.length != width * height) { + throw new IllegalArgumentException("Grid array length does not match width * height"); + } + } + + private static void processCell( + double[] grid, int width, int height, + double level, boolean normalize, List lineStrings, + int y, int x) { + double tl = grid[y * width + x]; + double tr = grid[y * width + (x + 1)]; + double br = grid[(y + 1) * width + (x + 1)]; + double bl = grid[(y + 1) * width + x]; + + int index = + (tl > level ? 1 : 0) | + (tr > level ? 2 : 0) | + (br > level ? 4 : 0) | + (bl > level ? 8 : 0); + + switch (index) { + case 1: + case 14: + createLineString( + grid, width, height, level, normalize, lineStrings, + x, y, x + 1, y, + x, y + 1, x, y); + break; + case 2: + case 13: + createLineString( + grid, width, height, level, normalize, lineStrings, + x + 1, y, x, y, + x + 1, y, x + 1, y + 1); + break; + case 3: + case 12: + createLineString( + grid, width, height, level, normalize, lineStrings, + x, y, x, y + 1, + x + 1, y, x + 1, y + 1); + break; + case 4: + case 11: + createLineString( + grid, width, height, level, normalize, lineStrings, + x + 1, y + 1, x + 1, y, + x + 1, y + 1, x, y + 1); + break; + case 5: + createLineString( + grid, width, height, level, normalize, lineStrings, + x, y, x, y + 1, + x, y + 1, x + 1, y + 1); + createLineString( + grid, width, height, level, normalize, lineStrings, + x + 1, y, x + 1, y + 1, + x + 1, y, x, y); + break; + case 6: + case 9: + createLineString( + grid, width, height, level, normalize, lineStrings, + x, y, x + 1, y, + x, y + 1, x + 1, y + 1); + break; + case 7: + case 8: + createLineString( + grid, width, height, level, normalize, lineStrings, + x, y, x, y + 1, + x, y + 1, x + 1, y + 1); + break; + case 10: + createLineString( + grid, width, height, level, normalize, lineStrings, + x, y, x + 1, y, + x + 1, y, x + 1, y + 1); + createLineString( + grid, width, height, level, normalize, lineStrings, + x + 1, y + 1, x, y + 1, + x, y + 1, x, y); + break; + } + } + + private static void createLineString( + double[] grid, int width, int height, + double level, boolean normalize, List lineStrings, + int x1, int y1, int x2, int y2, + int x3, int y3, int x4, int y4) { + Coordinate c1 = interpolate(grid, width, height, level, normalize, x1, y1, x2, y2); + Coordinate c2 = interpolate(grid, width, height, level, normalize, x3, y3, x4, y4); + lineStrings.add(GEOMETRY_FACTORY.createLineString(new Coordinate[] {c1, c2})); + } + + private static Coordinate interpolate( + double[] grid, int width, int height, + double level, boolean normalize, + int x1, int y1, int x2, int y2) { + double v1 = grid[y1 * width + x1]; + double v2 = grid[y2 * width + x2]; + double t = (Math.abs(v2 - v1) < EPSILON) ? 0.5 : (level - v1) / (v2 - v1); + double x = x1 + t * (x2 - x1); + double y = y1 + t * (y2 - y1); + if (normalize) { + x = x / (width - 1) * width; + y = y / (height - 1) * height; + } + return new Coordinate(x, y); + } +} diff --git a/baremaps-raster/src/main/java/org/apache/baremaps/raster/hillshade/HillShade.java b/baremaps-raster/src/main/java/org/apache/baremaps/raster/hillshade/HillShade.java deleted file mode 100644 index 104e2376f..000000000 --- a/baremaps-raster/src/main/java/org/apache/baremaps/raster/hillshade/HillShade.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.baremaps.raster.hillshade; - -public class HillShade { - - public static double[] hillShade(double[] dem, int width, int height, double sunAltitude, - double sunAzimuth) { - double[] hillshade = new double[dem.length]; - - double scale = 0.1; // Adjust the scale factor if needed - - // Convert sun altitude and azimuth from degrees to radians - double sunAltitudeRad = Math.toRadians(sunAltitude); - double sunAzimuthRad = Math.toRadians(sunAzimuth + 90); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - - // Handle edge cases for border pixels - int top = Math.max(y - 1, 0); - int left = Math.max(x - 1, 0); - int bottom = Math.min(y + 1, height - 1); - int right = Math.min(x + 1, width - 1); - - // Retrieve the elevation values from the 3x3 kernel - double z2 = dem[top * width + x]; - double z4 = dem[y * width + left]; - double z6 = dem[y * width + right]; - double z8 = dem[bottom * width + x]; - - // Calculate the dz/dx and dz/dy using the 3x3 kernel - double dzdx = (z6 - z4) / 2.0; - double dzdy = (z8 - z2) / 2.0; - - // Calculate the slope - double slope = Math.atan(scale * Math.sqrt(dzdx * dzdx + dzdy * dzdy)); - - // Calculate the aspect - double aspect = Math.atan2(dzdy, dzdx); - if (aspect < 0) { - aspect += 2 * Math.PI; - } - - // Calculate the reflectance - double reflectance = Math.cos(sunAltitudeRad) - * Math.cos(slope) - + Math.sin(sunAltitudeRad) - * Math.sin(slope) - * Math.cos(sunAzimuthRad - aspect); - - // Normalize the reflectance to be between 0 and 255 - hillshade[y * width + x] = Math.max(0, Math.min(255, reflectance * 255)); - } - } - - return hillshade; - } - - public static double[] hillShadeEnhanced(double[] dem, int width, int height, double sunAltitude, - double sunAzimuth) { - double[] hillshade = new double[dem.length]; - - double scale = 1.0; // Adjust the scale factor if needed - - // Convert sun altitude and azimuth from degrees to radians - double sunAltitudeRad = Math.toRadians(sunAltitude); - double sunAzimuthRad = Math.toRadians(sunAzimuth); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - - // Handle edge cases for border pixels - int top = Math.max(y - 1, 0); - int bottom = Math.min(y + 1, height - 1); - int left = Math.max(x - 1, 0); - int right = Math.min(x + 1, width - 1); - - // Retrieve the elevation values from the 3x3 kernel - double z1 = dem[top * width + left]; - double z2 = dem[top * width + x]; - double z3 = dem[top * width + right]; - double z4 = dem[y * width + left]; - double z6 = dem[y * width + right]; - double z7 = dem[bottom * width + left]; - double z8 = dem[bottom * width + x]; - double z9 = dem[bottom * width + right]; - - // Calculate the dz/dx and dz/dy using the 3x3 kernel - double dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) / 8.0; - double dzdy = ((z7 + 2 * z8 + z9) - (z1 + 2 * z2 + z3)) / 8.0; - - // Calculate the slope - double slope = Math.atan(scale * Math.sqrt(dzdx * dzdx + dzdy * dzdy)); - - // Calculate the aspect - double aspect = Math.atan2(dzdy, -dzdx); - if (aspect < 0) { - aspect += 2 * Math.PI; - } - - // Calculate the reflectance - double reflectance = Math.cos(sunAltitudeRad) - * Math.cos(slope) - + Math.sin(sunAltitudeRad) - * Math.sin(slope) - * Math.cos(sunAzimuthRad - aspect); - - // Normalize the reflectance to be between 0 and 255 - hillshade[y * width + x] = Math.max(0, Math.min(255, reflectance * 255)); - } - } - - return hillshade; - } -} diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/contour/IsoLineRenderer.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/contour/IsoLineRenderer.java deleted file mode 100644 index 9f1539b6e..000000000 --- a/baremaps-raster/src/test/java/org/apache/baremaps/raster/contour/IsoLineRenderer.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.baremaps.raster.contour; - -import static org.apache.baremaps.raster.contour.IsoLines.isoLines; - -import java.awt.*; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import javax.imageio.ImageIO; -import javax.swing.*; -import org.apache.baremaps.raster.contour.IsoLines.IsoLine; -import org.apache.baremaps.raster.martini.Martini; - -public class IsoLineRenderer { - - public static void main(String[] args) throws IOException { - var path = Path.of("") - .toAbsolutePath() - .resolveSibling("baremaps/baremaps-raster/src/test/resources/fuji.png") - .toAbsolutePath().toFile(); - - System.out.println(path); - - var image = ImageIO.read(path); - - double[] grid = Martini.grid(image); - List contours = new ArrayList<>(); - for (int i = 0; i < 8000; i += 100) { - contours.addAll(isoLines(grid, image.getWidth(), i)); - } - - // Create a frame to display the contours - JFrame frame = new JFrame("Contour Lines"); - frame.setSize(image.getWidth(), image.getHeight()); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.add(new ContourCanvas(contours)); - frame.setVisible(true); - } - - // Custom Canvas to draw the contours - static class ContourCanvas extends Canvas { - List contours; - - public ContourCanvas(List contours) { - this.contours = contours; - } - - @Override - public void paint(Graphics g) { - g.setColor(Color.RED); - for (IsoLine contour : contours) { - List points = contour.points() - .stream().map(p -> new Point((int) p.x(), (int) p.y())) - .toList(); - for (int i = 0; i < points.size() - 1; i++) { - Point p1 = points.get(i); - Point p2 = points.get(i + 1); - g.drawLine(p1.x, p1.y, p2.x, p2.y); - } - } - } - } -} diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeRenderer.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeRenderer.java new file mode 100644 index 000000000..ed0c57ca3 --- /dev/null +++ b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeRenderer.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.raster.elevation; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.apache.baremaps.raster.ElevationUtils; + +public class HillShadeRenderer extends JFrame { + + private BufferedImage originalImage; + private double[] grid; + private JSlider altitudeSlider; + private JSlider azimuthSlider; + private JSlider scaleSlider; + private JCheckBox isSimpleCheckbox; + private JLabel imageLabel; + private JLabel altitudeLabel; + private JLabel azimuthLabel; + private JLabel scaleLabel; + + public HillShadeRenderer() throws IOException { + super("Hillshade Display"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + // Load the image + originalImage = ImageIO.read( + Path.of("") + .toAbsolutePath() + .resolveSibling("baremaps/baremaps-raster/src/test/resources/fuji.png") + .toAbsolutePath().toFile()); + grid = ElevationUtils.imageToGrid(originalImage); + + // Create UI components + altitudeSlider = new JSlider(JSlider.VERTICAL, 0, 90, 45); + azimuthSlider = new JSlider(JSlider.VERTICAL, 0, 360, 315); + scaleSlider = new JSlider(JSlider.HORIZONTAL, 1, 100, 10); // Scale from 0.1 to 10.0 + isSimpleCheckbox = new JCheckBox("Simple Algorithm", true); + imageLabel = new JLabel(); + altitudeLabel = new JLabel("Sun Altitude: 45°"); + azimuthLabel = new JLabel("Sun Azimuth: 315°"); + scaleLabel = new JLabel("Scale: 1.0"); + + // Set up sliders + altitudeSlider.setMajorTickSpacing(15); + altitudeSlider.setPaintTicks(true); + altitudeSlider.setPaintLabels(true); + + azimuthSlider.setMajorTickSpacing(45); + azimuthSlider.setPaintTicks(true); + azimuthSlider.setPaintLabels(true); + + scaleSlider.setMajorTickSpacing(10); + scaleSlider.setPaintTicks(true); + scaleSlider.setPaintLabels(true); + + // Add listeners + ChangeListener listener = new ChangeListener() { + public void stateChanged(ChangeEvent e) { + updateLabels(); + redrawHillshade(); + } + }; + altitudeSlider.addChangeListener(listener); + azimuthSlider.addChangeListener(listener); + scaleSlider.addChangeListener(listener); + isSimpleCheckbox.addActionListener(e -> redrawHillshade()); + + // Set up layout + setLayout(new BorderLayout()); + JPanel controlPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.insets = new Insets(5, 5, 5, 5); + controlPanel.add(altitudeLabel, gbc); + gbc.gridy++; + controlPanel.add(altitudeSlider, gbc); + gbc.gridy++; + controlPanel.add(azimuthLabel, gbc); + gbc.gridy++; + controlPanel.add(azimuthSlider, gbc); + gbc.gridy++; + controlPanel.add(scaleLabel, gbc); + gbc.gridy++; + controlPanel.add(scaleSlider, gbc); + gbc.gridy++; + controlPanel.add(isSimpleCheckbox, gbc); + + add(imageLabel, BorderLayout.CENTER); + add(controlPanel, BorderLayout.EAST); + + // Initial draw + redrawHillshade(); + + pack(); + setVisible(true); + } + + private void updateLabels() { + altitudeLabel.setText("Sun Altitude: " + altitudeSlider.getValue() + "°"); + azimuthLabel.setText("Sun Azimuth: " + azimuthSlider.getValue() + "°"); + scaleLabel.setText("Scale: " + (scaleSlider.getValue() / 10.0)); + } + + private void redrawHillshade() { + int sunAltitude = altitudeSlider.getValue(); + int sunAzimuth = azimuthSlider.getValue(); + double scale = scaleSlider.getValue() / 10.0; + boolean isSimple = isSimpleCheckbox.isSelected(); + + double[] hillshade = HillShade.hillShade(grid, originalImage.getWidth(), + originalImage.getHeight(), sunAltitude, sunAzimuth, scale, isSimple); + + BufferedImage hillshadeImage = new BufferedImage(originalImage.getWidth(), + originalImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + for (int y = 0; y < originalImage.getHeight(); y++) { + for (int x = 0; x < originalImage.getWidth(); x++) { + int shade = (int) hillshade[y * originalImage.getWidth() + x]; + int rgb = new Color(shade, shade, shade).getRGB(); + hillshadeImage.setRGB(x, y, rgb); + } + } + + imageLabel.setIcon(new ImageIcon(hillshadeImage)); + revalidate(); + repaint(); + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + try { + new HillShadeRenderer(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } +} diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeTest.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeTest.java new file mode 100644 index 000000000..da572ae02 --- /dev/null +++ b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/HillShadeTest.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.raster.elevation; + +import static org.junit.jupiter.api.Assertions.*; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import javax.imageio.ImageIO; +import org.apache.baremaps.raster.ElevationUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.locationtech.jts.geom.LineString; + +class HillShadeTest { + + private static final double DELTA = 1e-6; + + @Test + @DisplayName("Test hillShade with valid input") + void testHillShadeValidInput() { + double[] dem = { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9 + }; + int width = 3; + int height = 3; + double sunAltitude = 45; + double sunAzimuth = 315; + + double[] result = HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth); + + assertNotNull(result); + assertEquals(dem.length, result.length); + } + + @Test + @DisplayName("Test hillShadeEnhanced with valid input") + void testHillShadeEnhancedValidInput() { + double[] dem = { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9 + }; + int width = 3; + int height = 3; + double sunAltitude = 45; + double sunAzimuth = 315; + + double[] result = HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth); + + assertNotNull(result); + assertEquals(dem.length, result.length); + } + + @ParameterizedTest + @MethodSource("provideInvalidInput") + @DisplayName("Test hillShade with invalid input") + void testHillShadeInvalidInput(double[] dem, int width, int height, double sunAltitude, + double sunAzimuth, Class expectedException) { + assertThrows(expectedException, + () -> HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth)); + } + + @ParameterizedTest + @MethodSource("provideInvalidInput") + @DisplayName("Test hillShadeEnhanced with invalid input") + void testHillShadeEnhancedInvalidInput(double[] dem, int width, int height, double sunAltitude, + double sunAzimuth, Class expectedException) { + assertThrows(expectedException, + () -> HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth)); + } + + private static Stream provideInvalidInput() { + return Stream.of( + Arguments.of(null, 3, 3, 45, 315, IllegalArgumentException.class), + Arguments.of(new double[0], 3, 3, 45, 315, IllegalArgumentException.class), + Arguments.of(new double[9], 0, 3, 45, 315, IllegalArgumentException.class), + Arguments.of(new double[9], 3, 0, 45, 315, IllegalArgumentException.class), + Arguments.of(new double[9], 2, 2, 45, 315, IllegalArgumentException.class), + Arguments.of(new double[9], 3, 3, -1, 315, IllegalArgumentException.class), + Arguments.of(new double[9], 3, 3, 91, 315, IllegalArgumentException.class), + Arguments.of(new double[9], 3, 3, 45, -1, IllegalArgumentException.class), + Arguments.of(new double[9], 3, 3, 45, 361, IllegalArgumentException.class)); + } + + @Test + @DisplayName("Test hillShade output range") + void testHillShadeOutputRange() { + double[] dem = new double[100]; + for (int i = 0; i < dem.length; i++) { + dem[i] = Math.random() * 1000; + } + int width = 10; + int height = 10; + double sunAltitude = 45; + double sunAzimuth = 315; + + double[] result = HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth); + + for (double value : result) { + assertTrue(value >= 0 && value <= 255, "Hillshade value should be between 0 and 255"); + } + } + + @Test + @DisplayName("Test hillShadeEnhanced output range") + void testHillShadeEnhancedOutputRange() { + double[] dem = new double[100]; + for (int i = 0; i < dem.length; i++) { + dem[i] = Math.random() * 1000; + } + int width = 10; + int height = 10; + double sunAltitude = 45; + double sunAzimuth = 315; + + double[] result = HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth); + + for (double value : result) { + assertTrue(value >= 0 && value <= 255, "Hillshade value should be between 0 and 255"); + } + } + + @Test + @DisplayName("Test hillShade with flat terrain") + void testHillShadeWithFlatTerrain() { + double[] dem = new double[9]; + Arrays.fill(dem, 100); + int width = 3; + int height = 3; + double sunAltitude = 90; + double sunAzimuth = 0; + + double[] result = HillShade.hillShade(dem, width, height, sunAltitude, sunAzimuth); + + for (double value : result) { + assertEquals(255, value, DELTA, + "Flat terrain with sun overhead should result in maximum brightness"); + } + } + + @Test + @DisplayName("Test hillShadeEnhanced with flat terrain") + void testHillShadeEnhancedWithFlatTerrain() { + double[] dem = new double[9]; + Arrays.fill(dem, 100); + int width = 3; + int height = 3; + double sunAltitude = 90; + double sunAzimuth = 0; + + double[] result = HillShade.hillShadeEnhanced(dem, width, height, sunAltitude, sunAzimuth); + + for (double value : result) { + assertEquals(255, value, DELTA, + "Flat terrain with sun overhead should result in maximum brightness"); + } + } + + @Test + @DisplayName("Test hillShade with fuji.png") + void testHillShadeWithFujiPng(@TempDir Path tempDir) throws IOException { + Path imagePath = Path.of("") + .toAbsolutePath() + .resolveSibling("baremaps-raster/src/test/resources/fuji.png") + .toAbsolutePath(); + var png = ImageIO.read(imagePath.toFile()); + assertNotNull(png, "Failed to load test image"); + + var grid = ElevationUtils.imageToGrid(png); + int width = png.getWidth(); + int height = png.getHeight(); + double sunAltitude = 45; + double sunAzimuth = 315; + + var hillshade = HillShade.hillShade(grid, width, height, sunAltitude, sunAzimuth); + + assertNotNull(hillshade, "Hillshade result should not be null"); + assertEquals(width * height, hillshade.length, + "Hillshade array size should match image dimensions"); + + var isoLines = new ArrayList(); + for (int i = 0; i < 255; i += 50) { + List lines = IsoLines.generateIsoLines(hillshade, width, height, i, true); + assertNotNull(lines, "Isoline generation should not return null"); + isoLines.addAll(lines); + } + + assertFalse(isoLines.isEmpty(), "At least one isoline should be generated"); + + BufferedImage hillshadeImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int gray = (int) hillshade[y * width + x]; + hillshadeImage.setRGB(x, y, (gray << 16) | (gray << 8) | gray); + } + } + Path outputPath = tempDir.resolve("fuji_hillshade.png"); + ImageIO.write(hillshadeImage, "png", outputPath.toFile()); + assertTrue(outputPath.toFile().exists(), "Hillshade image should be saved"); + } +} diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/IsoLinesRenderer.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/IsoLinesRenderer.java new file mode 100644 index 000000000..9643934df --- /dev/null +++ b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/IsoLinesRenderer.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.raster.elevation; + +import static org.apache.baremaps.raster.elevation.IsoLines.generateIsoLines; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import javax.imageio.ImageIO; +import javax.swing.*; +import org.apache.baremaps.raster.ElevationUtils; +import org.locationtech.jts.geom.LineString; + +public class IsoLinesRenderer { + + public static BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, + int targetHeight) throws IOException { + Image resultingImage = + originalImage.getScaledInstance(targetWidth, targetHeight, Image.SCALE_DEFAULT); + BufferedImage outputImage = + new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); + outputImage.getGraphics().drawImage(resultingImage, 0, 0, null); + return outputImage; + } + + public static void main(String[] args) throws IOException { + var path = Path.of("") + .toAbsolutePath() + .resolveSibling("baremaps/baremaps-raster/src/test/resources/fuji.png") + .toAbsolutePath().toFile(); + + var image1 = ImageIO.read(path); + double[] grid1 = ElevationUtils.imageToGrid(image1); + List contours1 = new ArrayList<>(); + for (int i = 0; i < 8000; i += 100) { + contours1.addAll(generateIsoLines(grid1, image1.getWidth(), image1.getHeight(), i, true)); + } + + // Downscale the image by 16 + var image2 = resizeImage(image1, 32, 32); + double[] grid2 = ElevationUtils.imageToGrid(image2); + List contours2 = new ArrayList<>(); + for (int i = 0; i < 8000; i += 100) { + for (LineString lineString : generateIsoLines(grid2, image2.getWidth(), image2.getHeight(), i, + true)) { + // Upscale the line string by 16 + lineString = (LineString) lineString.clone(); + for (int j = 0; j < lineString.getNumPoints(); j++) { + lineString.getCoordinates()[j].x *= 16; + lineString.getCoordinates()[j].y *= 16; + } + contours2.add(lineString); + + } + } + + // Create a frame to display the contours + JFrame frame = new JFrame("Contour Lines"); + frame.setSize(image1.getWidth(), image1.getHeight()); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(new ContourCanvas(image1, contours1, contours2)); + frame.setVisible(true); + } + + // Custom Canvas to draw the contours + static class ContourCanvas extends Canvas { + + Image image; + + List contours1; + + List contours2; + + public ContourCanvas(Image image, List contours1, List contours2) { + this.image = image; + this.contours1 = contours1; + this.contours2 = contours2; + } + + @Override + public void paint(Graphics g) { + + // Draw the image + g.drawImage(image, 0, 0, null); + + g.setColor(Color.BLACK); + for (LineString contour : contours1) { + List points = Stream.of(contour.getCoordinates()) + .map(p -> new Point((int) p.getX(), (int) p.getY())) + .toList(); + for (int i = 0; i < points.size() - 1; i++) { + Point p1 = points.get(i); + Point p2 = points.get(i + 1); + g.drawLine(p1.x, p1.y, p2.x, p2.y); + } + } + + g.setColor(Color.BLUE); + for (LineString contour : contours2) { + List points = Stream.of(contour.getCoordinates()) + .map(p -> new Point((int) p.getX(), (int) p.getY())) + .toList(); + for (int i = 0; i < points.size() - 1; i++) { + Point p1 = points.get(i); + Point p2 = points.get(i + 1); + g.drawLine(p1.x, p1.y, p2.x, p2.y); + } + } + + } + } +} diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/contour/IsoLinesTest.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/IsoLinesTest.java similarity index 71% rename from baremaps-raster/src/test/java/org/apache/baremaps/raster/contour/IsoLinesTest.java rename to baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/IsoLinesTest.java index 5955d1f0d..a3bdc1919 100644 --- a/baremaps-raster/src/test/java/org/apache/baremaps/raster/contour/IsoLinesTest.java +++ b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/IsoLinesTest.java @@ -15,16 +15,28 @@ * limitations under the License. */ -package org.apache.baremaps.raster.contour; +package org.apache.baremaps.raster.elevation; import java.io.IOException; import java.nio.file.Path; import javax.imageio.ImageIO; -import org.apache.baremaps.raster.martini.Martini; +import org.apache.baremaps.raster.ElevationUtils; import org.junit.jupiter.api.Test; class IsoLinesTest { + @Test + void grid1() throws IOException { + var grid = new double[] { + 0, 0, 0, + 0, 1, 0, + 0, 0, 0, + }; + var contour = IsoLines.generateIsoLines(grid, 3, 3, 0, true); + System.out.println(contour); + } + + @Test void contour() throws IOException { var png = ImageIO.read( @@ -32,8 +44,9 @@ void contour() throws IOException { .toAbsolutePath() .resolveSibling("baremaps-raster/src/test/resources/fuji.png") .toAbsolutePath().toFile()); - var terrainGrid = Martini.grid(png); - var contour = IsoLines.isoLines(terrainGrid, png.getWidth(), 500); + var terrainGrid = ElevationUtils.imageToGrid(png); + var contour = + IsoLines.generateIsoLines(terrainGrid, png.getWidth(), png.getHeight(), 500, true); System.out.println(contour); } } diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/hillshade/HillShadeRenderer.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/hillshade/HillShadeRenderer.java deleted file mode 100644 index 3112c6ecd..000000000 --- a/baremaps-raster/src/test/java/org/apache/baremaps/raster/hillshade/HillShadeRenderer.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.baremaps.raster.hillshade; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.nio.file.Path; -import javax.imageio.ImageIO; -import javax.swing.*; -import org.apache.baremaps.raster.ImageUtils; - -public class HillShadeRenderer { - - public static void main(String[] args) throws IOException { - var image = ImageIO.read( - Path.of("") - .toAbsolutePath() - .resolveSibling("baremaps/baremaps-raster/src/test/resources/fuji.png") - .toAbsolutePath().toFile()); - var grid = ImageUtils.grid(image); - var hillshade = HillShade.hillShade(grid, image.getWidth(), image.getHeight(), 45, 315); - - - // Create an output image - BufferedImage hillshadeImage = - new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - int shade = (int) hillshade[y * image.getWidth() + x]; - int rgb = new Color(shade, shade, shade).getRGB(); - hillshadeImage.setRGB(x, y, rgb); - } - } - - // Display the hillshade image in a JFrame - JFrame frame = new JFrame("Hillshade Display"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(image.getWidth(), image.getHeight()); - frame.add(new JLabel(new ImageIcon(hillshadeImage))); - frame.pack(); - frame.setVisible(true); - } -} diff --git a/baremaps-raster/src/test/java/org/apache/baremaps/raster/hillshade/HillShadeTest.java b/baremaps-raster/src/test/java/org/apache/baremaps/raster/hillshade/HillShadeTest.java deleted file mode 100644 index ec9c3265c..000000000 --- a/baremaps-raster/src/test/java/org/apache/baremaps/raster/hillshade/HillShadeTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.baremaps.raster.hillshade; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import javax.imageio.ImageIO; -import org.apache.baremaps.raster.contour.IsoLines; -import org.apache.baremaps.raster.contour.IsoLines.IsoLine; -import org.apache.baremaps.raster.martini.Martini; -import org.junit.jupiter.api.Test; - -class HillShadeTest { - - @Test - void hillshade() throws IOException { - var png = ImageIO.read( - Path.of("") - .toAbsolutePath() - .resolveSibling("baremaps-raster/src/test/resources/fuji.png") - .toAbsolutePath().toFile()); - var grid = Martini.grid(png); - var hillshade = HillShade.hillShade(grid, png.getWidth(), png.getHeight(), 45, 315); - var isoLines = new ArrayList(); - for (int i = 0; i < 255; i += 50) { - isoLines.addAll(IsoLines.isoLines(hillshade, png.getWidth(), i)); - } - - System.out.println(isoLines); - } - -} diff --git a/baremaps-server/src/main/resources/raster/favicon.ico b/baremaps-server/src/main/resources/raster/favicon.ico new file mode 100644 index 000000000..7162b07e3 Binary files /dev/null and b/baremaps-server/src/main/resources/raster/favicon.ico differ diff --git a/baremaps-server/src/main/resources/raster/hillshade.html b/baremaps-server/src/main/resources/raster/hillshade.html new file mode 100644 index 000000000..23dd18199 --- /dev/null +++ b/baremaps-server/src/main/resources/raster/hillshade.html @@ -0,0 +1,57 @@ + + + + 3D Terrain + + + + + + + + +
+ + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 155b42c35..ed38acf7e 100644 --- a/pom.xml +++ b/pom.xml @@ -116,6 +116,7 @@ limitations under the License. 2.0.12 3.45.1.0 1.19.6 + 3.11.0 0.8.11 3.0.0 3.10.1 @@ -137,7 +138,6 @@ limitations under the License. - com.fasterxml.jackson.core jackson-annotations @@ -197,7 +197,7 @@ limitations under the License. com.twelvemonkeys.imageio imageio-tiff - 3.11.0 + ${version.lib.twelvemonkeys} com.zaxxer