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