ElevationModel.java
/*
* Copyright (c) 2019 Martin Davis.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jts.operation.overlayng;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateSequenceFilter;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.math.MathUtil;
/**
* A simple elevation model used to populate missing Z values
* in overlay results.
* <p>
* The model divides the extent of the input geometry(s)
* into an NxM grid.
* The default grid size is 3x3.
* If the input has no extent in the X or Y dimension,
* that dimension is given grid size 1.
* The elevation of each grid cell is computed as the average of the Z values
* of the input vertices in that cell (if any).
* If a cell has no input vertices within it, it is assigned
* the average elevation over all cells.
* <p>
* If no input vertices have Z values, the model does not assign a Z value.
* <p>
* The elevation of an arbitrary location is determined as the
* Z value of the nearest grid cell.
* <p>
* An elevation model can be used to populate missing Z values
* in an overlay result geometry.
*
* @author Martin Davis
*
*/
class ElevationModel {
private static final int DEFAULT_CELL_NUM = 3;
/**
* Creates an elevation model from two geometries (which may be null).
*
* @param geom1 an input geometry
* @param geom2 an input geometry, or null
* @return the elevation model computed from the geometries
*/
public static ElevationModel create(Geometry geom1, Geometry geom2) {
Envelope extent = geom1.getEnvelopeInternal().copy();
if (geom2 != null) {
extent.expandToInclude(geom2.getEnvelopeInternal());
}
ElevationModel model = new ElevationModel(extent, DEFAULT_CELL_NUM, DEFAULT_CELL_NUM);
if (geom1 != null) model.add(geom1);
if (geom2 != null) model.add(geom2);
return model;
}
private Envelope extent;
private int numCellX;
private int numCellY;
private double cellSizeX;
private double cellSizeY;
private ElevationCell[][] cells;
private boolean isInitialized = false;
private boolean hasZValue = false;
private double averageZ = Double.NaN;
/**
* Creates a new elevation model covering an extent by a grid of given dimensions.
*
* @param extent the XY extent to cover
* @param numCellX the number of grid cells in the X dimension
* @param numCellY the number of grid cells in the Y dimension
*/
public ElevationModel(Envelope extent, int numCellX, int numCellY) {
this.extent = extent;
this.numCellX = numCellX;
this.numCellY = numCellY;
cellSizeX = extent.getWidth() / numCellX;
cellSizeY = extent.getHeight() / numCellY;
if(cellSizeX <= 0.0) {
this.numCellX = 1;
}
if(cellSizeY <= 0.0) {
this.numCellY = 1;
}
cells = new ElevationCell[numCellX][numCellY];
}
/**
* Updates the model using the Z values of a given geometry.
*
* @param geom the geometry to scan for Z values
*/
public void add(Geometry geom) {
geom.apply(new CoordinateSequenceFilter() {
private boolean hasZ = true;
@Override
public void filter(CoordinateSequence seq, int i) {
if (! seq.hasZ()) {
hasZ = false;;
return;
}
double z = seq.getOrdinate(i, Coordinate.Z);
add(seq.getOrdinate(i, Coordinate.X),
seq.getOrdinate(i, Coordinate.Y),
z);
}
@Override
public boolean isDone() {
// no need to scan if no Z present
return ! hasZ;
}
@Override
public boolean isGeometryChanged() {
return false;
}
});
}
protected void add(double x, double y, double z) {
if (Double.isNaN(z))
return;
hasZValue = true;
ElevationCell cell = getCell(x, y, true);
cell.add(z);
}
private void init() {
isInitialized = true;
int numCells = 0;
double sumZ = 0.0;
for (int i = 0; i < cells.length; i++) {
for (int j = 0; j < cells[0].length; j++) {
ElevationCell cell = cells[i][j];
if (cell != null) {
cell.compute();
numCells++;
sumZ += cell.getZ();
}
}
}
averageZ = Double.NaN;
if (numCells > 0) {
averageZ = sumZ / numCells;
}
}
/**
* Gets the model Z value at a given location.
* If the location lies outside the model grid extent,
* this returns the Z value of the nearest grid cell.
* If the model has no elevation computed (i.e. due
* to empty input), the value is returned as {@link Double#NaN}.
*
* @param x the x ordinate of the location
* @param y the y ordinate of the location
* @return the computed model Z value
*/
public double getZ(double x, double y) {
if (! isInitialized)
init();
ElevationCell cell = getCell(x, y, false);
if (cell == null)
return averageZ;
return cell.getZ();
}
/**
* Computes Z values for any missing Z values in a geometry,
* using the computed model.
* If the model has no Z value, or the geometry coordinate dimension
* does not include Z, the geometry is not updated.
*
* @param geom the geometry to populate Z values for
*/
public void populateZ(Geometry geom) {
// short-circuit if no Zs are present in model
if (! hasZValue)
return;
if (! isInitialized)
init();
geom.apply(new CoordinateSequenceFilter() {
private boolean isDone = false;
@Override
public void filter(CoordinateSequence seq, int i) {
if (!seq.hasZ()) {
// if no Z then short-circuit evaluation
isDone = true;
return;
}
// if Z not populated then assign using model
if (Double.isNaN( seq.getZ(i) )) {
double z = getZ(seq.getOrdinate(i, Coordinate.X),
seq.getOrdinate(i, Coordinate.Y));
seq.setOrdinate(i, Coordinate.Z, z);
}
}
@Override
public boolean isDone() {
return isDone;
}
@Override
public boolean isGeometryChanged() {
// geometry extent is not changed
return false;
}
});
}
private ElevationCell getCell(double x, double y, boolean isCreateIfMissing) {
int ix = 0;
if (numCellX > 1) {
ix = (int) ((x - extent.getMinX()) / cellSizeX);
ix = MathUtil.clamp(ix, 0, numCellX - 1);
}
int iy = 0;
if (numCellY > 1) {
iy = (int) ((y - extent.getMinY()) / cellSizeY);
iy = MathUtil.clamp(iy, 0, numCellY - 1);
}
ElevationCell cell = cells[ix][iy];
if (isCreateIfMissing && cell == null) {
cell = new ElevationCell();
cells[ix][iy] = cell;
}
return cell;
}
static class ElevationCell {
private int numZ = 0;
private double sumZ = 0.0;
private double avgZ;
public void add(double z) {
numZ++;
sumZ += z;
}
public void compute() {
avgZ = Double.NaN;
if (numZ > 0)
avgZ = sumZ / numZ;
}
public double getZ() {
return avgZ;
}
}
}