1 /* 2 * Copyright (c) 2019 Martin Davis. 3 * 4 * All rights reserved. This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * and Eclipse Distribution License v. 1.0 which accompanies this distribution. 7 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html 8 * and the Eclipse Distribution License is available at 9 * 10 * http://www.eclipse.org/org/documents/edl-v10.php. 11 */ 12 package org.locationtech.jts.operation.overlayng; 13 14 import org.locationtech.jts.geom.Coordinate; 15 import org.locationtech.jts.geom.CoordinateSequence; 16 import org.locationtech.jts.geom.CoordinateSequenceFilter; 17 import org.locationtech.jts.geom.Envelope; 18 import org.locationtech.jts.geom.Geometry; 19 import org.locationtech.jts.math.MathUtil; 20 21 /** 22 * A simple elevation model used to populate missing Z values 23 * in overlay results. 24 * <p> 25 * The model divides the extent of the input geometry(s) 26 * into an NxM grid. 27 * The default grid size is 3x3. 28 * If the input has no extent in the X or Y dimension, 29 * that dimension is given grid size 1. 30 * The elevation of each grid cell is computed as the average of the Z values 31 * of the input vertices in that cell (if any). 32 * If a cell has no input vertices within it, it is assigned 33 * the average elevation over all cells. 34 * <p> 35 * If no input vertices have Z values, the model does not assign a Z value. 36 * <p> 37 * The elevation of an arbitrary location is determined as the 38 * Z value of the nearest grid cell. 39 * <p> 40 * An elevation model can be used to populate missing Z values 41 * in an overlay result geometry. 42 * 43 * @author Martin Davis 44 * 45 */ 46 class ElevationModel { 47 48 private static final int DEFAULT_CELL_NUM = 3; 49 50 /** 51 * Creates an elevation model from two geometries (which may be null). 52 * 53 * @param geom1 an input geometry 54 * @param geom2 an input geometry, or null 55 * @return the elevation model computed from the geometries 56 */ create(Geometry geom1, Geometry geom2)57 public static ElevationModel create(Geometry geom1, Geometry geom2) { 58 Envelope extent = geom1.getEnvelopeInternal().copy(); 59 if (geom2 != null) { 60 extent.expandToInclude(geom2.getEnvelopeInternal()); 61 } 62 ElevationModel model = new ElevationModel(extent, DEFAULT_CELL_NUM, DEFAULT_CELL_NUM); 63 if (geom1 != null) model.add(geom1); 64 if (geom2 != null) model.add(geom2); 65 return model; 66 } 67 68 private Envelope extent; 69 private int numCellX; 70 private int numCellY; 71 private double cellSizeX; 72 private double cellSizeY; 73 private ElevationCell[][] cells; 74 private boolean isInitialized = false; 75 private boolean hasZValue = false; 76 private double averageZ = Double.NaN; 77 78 /** 79 * Creates a new elevation model covering an extent by a grid of given dimensions. 80 * 81 * @param extent the XY extent to cover 82 * @param numCellX the number of grid cells in the X dimension 83 * @param numCellY the number of grid cells in the Y dimension 84 */ ElevationModel(Envelope extent, int numCellX, int numCellY)85 public ElevationModel(Envelope extent, int numCellX, int numCellY) { 86 this.extent = extent; 87 this.numCellX = numCellX; 88 this.numCellY = numCellY; 89 90 cellSizeX = extent.getWidth() / numCellX; 91 cellSizeY = extent.getHeight() / numCellY; 92 if(cellSizeX <= 0.0) { 93 this.numCellX = 1; 94 } 95 if(cellSizeY <= 0.0) { 96 this.numCellY = 1; 97 } 98 cells = new ElevationCell[numCellX][numCellY]; 99 } 100 101 /** 102 * Updates the model using the Z values of a given geometry. 103 * 104 * @param geom the geometry to scan for Z values 105 */ add(Geometry geom)106 public void add(Geometry geom) { 107 geom.apply(new CoordinateSequenceFilter() { 108 109 private boolean hasZ = true; 110 111 @Override 112 public void filter(CoordinateSequence seq, int i) { 113 if (! seq.hasZ()) { 114 hasZ = false;; 115 return; 116 } 117 double z = seq.getOrdinate(i, Coordinate.Z); 118 add(seq.getOrdinate(i, Coordinate.X), 119 seq.getOrdinate(i, Coordinate.Y), 120 z); 121 } 122 123 @Override 124 public boolean isDone() { 125 // no need to scan if no Z present 126 return ! hasZ; 127 } 128 129 @Override 130 public boolean isGeometryChanged() { 131 return false; 132 } 133 134 }); 135 } 136 add(double x, double y, double z)137 protected void add(double x, double y, double z) { 138 if (Double.isNaN(z)) 139 return; 140 hasZValue = true; 141 ElevationCell cell = getCell(x, y, true); 142 cell.add(z); 143 } 144 init()145 private void init() { 146 isInitialized = true; 147 int numCells = 0; 148 double sumZ = 0.0; 149 150 for (int i = 0; i < cells.length; i++) { 151 for (int j = 0; j < cells[0].length; j++) { 152 ElevationCell cell = cells[i][j]; 153 if (cell != null) { 154 cell.compute(); 155 numCells++; 156 sumZ += cell.getZ(); 157 } 158 } 159 } 160 averageZ = Double.NaN; 161 if (numCells > 0) { 162 averageZ = sumZ / numCells; 163 } 164 } 165 166 /** 167 * Gets the model Z value at a given location. 168 * If the location lies outside the model grid extent, 169 * this returns the Z value of the nearest grid cell. 170 * If the model has no elevation computed (i.e. due 171 * to empty input), the value is returned as {@link Double#NaN}. 172 * 173 * @param x the x ordinate of the location 174 * @param y the y ordinate of the location 175 * @return the computed model Z value 176 */ getZ(double x, double y)177 public double getZ(double x, double y) { 178 if (! isInitialized) 179 init(); 180 ElevationCell cell = getCell(x, y, false); 181 if (cell == null) 182 return averageZ; 183 return cell.getZ(); 184 } 185 186 /** 187 * Computes Z values for any missing Z values in a geometry, 188 * using the computed model. 189 * If the model has no Z value, or the geometry coordinate dimension 190 * does not include Z, the geometry is not updated. 191 * 192 * @param geom the geometry to populate Z values for 193 */ populateZ(Geometry geom)194 public void populateZ(Geometry geom) { 195 // short-circuit if no Zs are present in model 196 if (! hasZValue) 197 return; 198 199 if (! isInitialized) 200 init(); 201 202 geom.apply(new CoordinateSequenceFilter() { 203 204 private boolean isDone = false; 205 206 @Override 207 public void filter(CoordinateSequence seq, int i) { 208 if (!seq.hasZ()) { 209 // if no Z then short-circuit evaluation 210 isDone = true; 211 return; 212 } 213 // if Z not populated then assign using model 214 if (Double.isNaN( seq.getZ(i) )) { 215 double z = getZ(seq.getOrdinate(i, Coordinate.X), 216 seq.getOrdinate(i, Coordinate.Y)); 217 seq.setOrdinate(i, Coordinate.Z, z); 218 } 219 } 220 221 @Override 222 public boolean isDone() { 223 return isDone; 224 } 225 226 @Override 227 public boolean isGeometryChanged() { 228 // geometry extent is not changed 229 return false; 230 } 231 232 }); 233 } 234 getCell(double x, double y, boolean isCreateIfMissing)235 private ElevationCell getCell(double x, double y, boolean isCreateIfMissing) { 236 int ix = 0; 237 if (numCellX > 1) { 238 ix = (int) ((x - extent.getMinX()) / cellSizeX); 239 ix = MathUtil.clamp(ix, 0, numCellX - 1); 240 } 241 int iy = 0; 242 if (numCellY > 1) { 243 iy = (int) ((y - extent.getMinY()) / cellSizeY); 244 iy = MathUtil.clamp(iy, 0, numCellY - 1); 245 } 246 ElevationCell cell = cells[ix][iy]; 247 if (isCreateIfMissing && cell == null) { 248 cell = new ElevationCell(); 249 cells[ix][iy] = cell; 250 } 251 return cell; 252 } 253 254 static class ElevationCell { 255 256 private int numZ = 0; 257 private double sumZ = 0.0; 258 private double avgZ; 259 add(double z)260 public void add(double z) { 261 numZ++; 262 sumZ += z; 263 } 264 compute()265 public void compute() { 266 avgZ = Double.NaN; 267 if (numZ > 0) 268 avgZ = sumZ / numZ; 269 } 270 getZ()271 public double getZ() { 272 return avgZ; 273 } 274 } 275 } 276