All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.baremaps.dem.Martini Maven / Gradle / Ivy

The newest version!
/*
 * 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.dem;

import java.awt.image.BufferedImage;

/**
 * The {@code Martini} class implements the MARTINI algorithm for generating 3D terrain meshes from
 * height data.
 *
 * @see Martini GitHub
 */
public class Martini {

  private static final double ELEVATION_OFFSET = 10000.0;

  private final int gridSize;
  private final int numTriangles;
  private final int numParentTriangles;
  private final int[] baseCoords;

  /**
   * Constructs a new {@code Martini} instance with the specified grid size.
   *
   * @param gridSize the grid size (must be 2^n+1)
   * @throws IllegalArgumentException if the grid size is invalid
   */
  public Martini(int gridSize) {
    this.gridSize = gridSize;

    int tileSize = gridSize - 1;
    if ((tileSize & (tileSize - 1)) != 0) {
      throw new IllegalArgumentException("Expected grid size to be 2^n+1, got " + gridSize + ".");
    }

    this.numTriangles = tileSize * tileSize * 2 - 2;
    this.numParentTriangles = this.numTriangles - tileSize * tileSize;

    this.baseCoords = new int[this.numTriangles * 4];
    for (int i = 0; i < this.numTriangles; i++) {
      int id = i + 2;
      int ax = 0;
      int ay = 0;
      int bx = 0;
      int by = 0;
      int cx = 0;
      int cy = 0;
      if ((id & 1) != 0) {
        bx = by = cx = tileSize;
      } else {
        ax = ay = cy = tileSize;
      }
      while ((id >>= 1) > 1) {
        int mx = (ax + bx) >> 1;
        int my = (ay + by) >> 1;

        if ((id & 1) != 0) {
          bx = ax;
          by = ay;
          ax = cx;
          ay = cy;
        } else {
          ax = bx;
          ay = by;
          bx = cx;
          by = cy;
        }
        cx = mx;
        cy = my;
      }
      int k = i * 4;
      this.baseCoords[k] = ax;
      this.baseCoords[k + 1] = ay;
      this.baseCoords[k + 2] = bx;
      this.baseCoords[k + 3] = by;
    }
  }

  /**
   * Creates a terrain grid from a BufferedImage.
   *
   * @param image the input image
   * @return a double array representing the terrain grid
   */
  public static double[] createGrid(BufferedImage image) {
    int gridSize = image.getWidth() + 1;
    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 * 256 + g * 256.0f + b) / 10.0f - ELEVATION_OFFSET;
      }
    }

    // backfill right and bottom borders
    for (int x = 0; x < gridSize - 1; x++) {
      terrain[gridSize * (gridSize - 1) + x] = terrain[gridSize * (gridSize - 2) + x];
    }
    for (int y = 0; y < gridSize; y++) {
      terrain[gridSize * y + gridSize - 1] = terrain[gridSize * y + gridSize - 2];
    }

    return terrain;
  }

  /**
   * Creates a new {@code Tile} instance with the specified terrain data.
   *
   * @param terrain the terrain data
   * @return the tile
   * @throws IllegalArgumentException if the terrain data is invalid
   */
  public Tile createTile(double[] terrain) {
    return new Tile(terrain, gridSize, numTriangles, numParentTriangles, baseCoords);
  }

  /**
   * The {@code Tile} class represents a tile of terrain data.
   */
  public static class Tile {

    private final int gridSize;
    private final int[] indices;
    private final double[] errors;

    private int numVertices;
    private int numTriangles;

    private int[] vertices;
    private int[] triangles;
    private int triIndex = 0;

    private Tile(
        double[] terrain,
        int gridSize,
        int numTriangles,
        int numParentTriangles,
        int[] coords) {
      if (terrain.length != gridSize * gridSize) {
        throw new IllegalArgumentException(
            "Expected terrain data of length " + (gridSize * gridSize) + " (" + gridSize + " x "
                + gridSize + "), got " + terrain.length + ".");
      }

      this.gridSize = gridSize;
      this.indices = new int[this.gridSize * this.gridSize];

      this.errors = new double[terrain.length];
      for (int i = numTriangles - 1; i >= 0; i--) {
        int k = i * 4;
        int ax = coords[k];
        int ay = coords[k + 1];
        int bx = coords[k + 2];
        int by = coords[k + 3];
        int mx = (ax + bx) >> 1;
        int my = (ay + by) >> 1;
        int cx = mx + my - ay;
        int cy = my + ax - mx;

        double interpolatedHeight = (terrain[ay * gridSize + ax] + terrain[by * gridSize + bx]) / 2;
        int middleIndex = my * gridSize + mx;
        double middleError = Math.abs(interpolatedHeight - terrain[middleIndex]);

        errors[middleIndex] = Math.max(errors[middleIndex], middleError);

        if (i < numParentTriangles) {
          int leftChildIndex = ((ay + cy) >> 1) * gridSize + ((ax + cx) >> 1);
          int rightChildIndex = ((by + cy) >> 1) * gridSize + ((bx + cx) >> 1);
          errors[middleIndex] = Math.max(errors[middleIndex],
              Math.max(errors[leftChildIndex], errors[rightChildIndex]));
        }
      }
    }

    /**
     * Returns the mesh of the tile with the specified maximum error.
     *
     * @param maxError the maximum error
     * @return the mesh
     */
    public Mesh getMesh(double maxError) {
      int max = gridSize - 1;

      numVertices = 0;
      numTriangles = 0;
      countElements(0, 0, max, max, max, 0, maxError);
      countElements(max, max, 0, 0, 0, max, maxError);

      vertices = new int[numVertices * 2];
      triangles = new int[numTriangles * 3];
      triIndex = 0;
      processTriangle(0, 0, max, max, max, 0, maxError);
      processTriangle(max, max, 0, 0, 0, max, maxError);

      return new Mesh(vertices, triangles);
    }

    private void countElements(int ax, int ay, int bx, int by, int cx, int cy, double maxError) {
      int mx = (ax + bx) >> 1;
      int my = (ay + by) >> 1;

      if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * gridSize + mx] > maxError) {
        countElements(cx, cy, ax, ay, mx, my, maxError);
        countElements(bx, by, cx, cy, mx, my, maxError);
      } else {
        indices[ay * gridSize + ax] =
            indices[ay * gridSize + ax] != 0 ? indices[ay * gridSize + ax] : ++numVertices;
        indices[by * gridSize + bx] =
            indices[by * gridSize + bx] != 0 ? indices[by * gridSize + bx] : ++numVertices;
        indices[cy * gridSize + cx] =
            indices[cy * gridSize + cx] != 0 ? indices[cy * gridSize + cx] : ++numVertices;
        numTriangles++;
      }
    }

    private void processTriangle(int ax, int ay, int bx, int by, int cx, int cy, double maxError) {
      int mx = (ax + bx) >> 1;
      int my = (ay + by) >> 1;

      if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * gridSize + mx] > maxError) {
        processTriangle(cx, cy, ax, ay, mx, my, maxError);
        processTriangle(bx, by, cx, cy, mx, my, maxError);
      } else {
        int a = indices[ay * gridSize + ax] - 1;
        int b = indices[by * gridSize + bx] - 1;
        int c = indices[cy * gridSize + cx] - 1;

        vertices[2 * a] = ax;
        vertices[2 * a + 1] = ay;
        vertices[2 * b] = bx;
        vertices[2 * b + 1] = by;
        vertices[2 * c] = cx;
        vertices[2 * c + 1] = cy;

        triangles[triIndex++] = a;
        triangles[triIndex++] = b;
        triangles[triIndex++] = c;
      }
    }
  }

  /**
   * The {@code Mesh} class represents a mesh of vertices and triangles.
   */
  public static class Mesh {

    private final int[] vertices;
    private final int[] triangles;

    private Mesh(int[] vertices, int[] triangles) {
      this.vertices = vertices;
      this.triangles = triangles;
    }

    /**
     * Returns the vertices of the mesh.
     *
     * @return an array of vertex coordinates
     */
    public int[] getVertices() {
      return vertices;
    }

    /**
     * Returns the triangles of the mesh.
     *
     * @return an array of triangle indices
     */
    public int[] getTriangles() {
      return triangles;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy