001/* =========================================================== 002 * Orson Charts : a 3D chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C)opyright 2013-2022, by David Gilbert. All rights reserved. 006 * 007 * https://github.com/jfree/orson-charts 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * Orson Charts home page: 028 * 029 * http://www.object-refinery.com/orsoncharts/index.html 030 * 031 */ 032 033package org.jfree.chart3d.renderer.xyz; 034 035import java.awt.Color; 036import java.io.Serializable; 037import java.util.ArrayList; 038import java.util.List; 039 040import org.jfree.chart3d.axis.ValueAxis3D; 041import org.jfree.chart3d.data.Range; 042import org.jfree.chart3d.data.function.Function3D; 043import org.jfree.chart3d.data.function.Function3DUtils; 044import org.jfree.chart3d.data.xyz.XYZDataset; 045import org.jfree.chart3d.graphics3d.Dimension3D; 046import org.jfree.chart3d.graphics3d.Object3D; 047import org.jfree.chart3d.graphics3d.Point3D; 048import org.jfree.chart3d.graphics3d.World; 049import org.jfree.chart3d.internal.Args; 050import org.jfree.chart3d.plot.XYZPlot; 051import org.jfree.chart3d.renderer.ColorScale; 052import org.jfree.chart3d.renderer.ColorScaleRenderer; 053import org.jfree.chart3d.renderer.ComposeType; 054import org.jfree.chart3d.renderer.FixedColorScale; 055import org.jfree.chart3d.renderer.Renderer3DChangeEvent; 056 057/** 058 * A renderer that plots a surface based on a function (any implementation 059 * of {@link Function3D}). This renderer is different to others in that it 060 * does not plot data from a dataset, instead it samples a function and plots 061 * those values. By default 900 samples are taken (30 x-values by 30 z-values) 062 * although this can be modified. 063 * <br><br> 064 * For the fastest rendering, the {@code drawFaceOutlines} flag can be set 065 * to {@code false} (the default is {@code true}) but this may 066 * cause slight rendering artifacts if anti-aliasing is on (note that switching 067 * off anti-aliasing as well also improves rendering performance). 068 * <br><br> 069 * NOTE: This class is serializable, but the serialization format is subject 070 * to change in future releases and should not be relied upon for persisting 071 * instances of this class. 072 * 073 * @since 1.1 074 */ 075@SuppressWarnings("serial") 076public class SurfaceRenderer extends AbstractXYZRenderer implements XYZRenderer, 077 ColorScaleRenderer, Serializable { 078 079 /** The function. */ 080 private Function3D function; 081 082 /** The number of samples along the x-axis (minimum 2). */ 083 private int xSamples; 084 085 /** The number of samples along the z-axis (minimum 2). */ 086 private int zSamples; 087 088 /** The color scale. */ 089 private ColorScale colorScale; 090 091 /** 092 * A flag that controls whether the faces that make up the surface have 093 * their outlines drawn (in addition to the shape being filled). The 094 * default value is {@code true} which renders a solid surface but 095 * is slower. 096 */ 097 private boolean drawFaceOutlines; 098 099 /** 100 * Creates a new renderer for the specified function. By default, the 101 * renderer will take 30 samples along the x-axis and 30 samples along the 102 * z-axis (this is configurable). 103 * 104 * @param function the function ({@code null} not permitted). 105 */ 106 public SurfaceRenderer(Function3D function) { 107 Args.nullNotPermitted(function, "function"); 108 this.function = function; 109 this.xSamples = 30; 110 this.zSamples = 30; 111 this.colorScale = new FixedColorScale(Color.YELLOW); 112 this.drawFaceOutlines = true; 113 } 114 115 /** 116 * Returns the number of samples the renderer will take along the 117 * x-axis when plotting the function. The default value is 30. 118 * 119 * @return The number of samples. 120 */ 121 public int getXSamples() { 122 return this.xSamples; 123 } 124 125 /** 126 * Sets the number of samples the renderer will take along the x-axis when 127 * plotting the function and sends a {@link Renderer3DChangeEvent} to all 128 * registered listeners. The default value is 30, setting this to higher 129 * values will result in smoother looking plots, but they will take 130 * longer to draw. 131 * 132 * @param count the count. 133 * 134 * @see #setZSamples(int) 135 */ 136 public void setXSamples(int count) { 137 this.xSamples = count; 138 fireChangeEvent(true); 139 } 140 141 /** 142 * Returns the number of samples the renderer will take along the z-axis 143 * when plotting the function and sends a {@link Renderer3DChangeEvent} to 144 * all registered listeners. The default value is 30. 145 * 146 * @return The number of samples. 147 */ 148 public int getZSamples() { 149 return this.zSamples; 150 } 151 152 /** 153 * Sets the number of samples the renderer will take along the z-axis when 154 * plotting the function and sends a {@link Renderer3DChangeEvent} to all 155 * registered listeners. The default value is 30, setting this to higher 156 * values will result in smoother looking plots, but they will take 157 * longer to draw. 158 * 159 * @param count the count. 160 * 161 * @see #setXSamples(int) 162 */ 163 public void setZSamples(int count) { 164 this.zSamples = count; 165 } 166 167 /** 168 * Returns the compose-type for the renderer. Here the value is 169 * {@code ComposeType.ALL} which means the plot will call the 170 * {@link #composeAll(org.jfree.chart3d.plot.XYZPlot, 171 * org.jfree.chart3d.graphics3d.World, org.jfree.chart3d.graphics3d.Dimension3D, 172 * double, double, double)} method for composing the chart. 173 * 174 * @return The compose type (never {@code null}). 175 */ 176 @Override 177 public ComposeType getComposeType() { 178 return ComposeType.ALL; 179 } 180 181 /** 182 * Returns the color scale. This determines the color of the surface 183 * according to the y-value. 184 * 185 * @return The color scale (never {@code null}). 186 */ 187 @Override 188 public ColorScale getColorScale() { 189 return this.colorScale; 190 } 191 192 /** 193 * Sets the color scale and sends a {@link Renderer3DChangeEvent} to all 194 * registered listeners. 195 * 196 * @param colorScale the color scale ({@code null} not permitted). 197 */ 198 public void setColorScale(ColorScale colorScale) { 199 Args.nullNotPermitted(colorScale, "colorScale"); 200 this.colorScale = colorScale; 201 fireChangeEvent(true); 202 } 203 204 /** 205 * Returns the flag that controls whether or not the faces that make 206 * up the surface have their outlines drawn during rendering. The 207 * default value is {@code true}. 208 * 209 * @return A boolean. 210 */ 211 public boolean getDrawFaceOutlines() { 212 return this.drawFaceOutlines; 213 } 214 215 /** 216 * Sets a flag that controls whether or not the faces that make up the 217 * surface are drawn (as well as filled) and sends a 218 * {@link Renderer3DChangeEvent} to all registered listeners. If the face 219 * outlines are drawn (the default), the surface is solid (but takes longer 220 * to draw). If the face outlines are not drawn, Java2D can leave small 221 * gaps that you can "see" through (if you don't mind this, then the 222 * performance is better). 223 * 224 * @param draw the new flag value. 225 */ 226 public void setDrawFaceOutlines(boolean draw) { 227 this.drawFaceOutlines = draw; 228 fireChangeEvent(true); 229 } 230 231 /** 232 * Composes the entire representation of the function in the supplied 233 * {@code world}. 234 * 235 * @param plot the plot. 236 * @param world the world. 237 * @param dimensions the plot dimensions. 238 * @param xOffset the x-offset. 239 * @param yOffset the y-offset. 240 * @param zOffset the z-offset. 241 */ 242 @Override 243 public void composeAll(XYZPlot plot, World world, Dimension3D dimensions, 244 double xOffset, double yOffset, double zOffset) { 245 246 // need to know the x-axis range and the z-axis range 247 ValueAxis3D xAxis = plot.getXAxis(); 248 ValueAxis3D yAxis = plot.getYAxis(); 249 ValueAxis3D zAxis = plot.getZAxis(); 250 Dimension3D dim = plot.getDimensions(); 251 double xlen = dim.getWidth(); 252 double ylen = dim.getHeight(); 253 double zlen = dim.getDepth(); 254 Range yRange = new Range(yOffset, -yOffset); 255 for (int xIndex = 0; xIndex < this.xSamples; xIndex++) { 256 double xfrac0 = xIndex / (double) this.xSamples; 257 double xfrac1 = (xIndex + 1) / (double) this.xSamples; 258 for (int zIndex = 0; zIndex < this.zSamples; zIndex++) { 259 double zfrac0 = zIndex / (double) this.zSamples; 260 double zfrac1 = (zIndex + 1) / (double) this.zSamples; 261 262 double x0 = xAxis.getRange().value(xfrac0); 263 double x1 = xAxis.getRange().value(xfrac1); 264 double xm = x0 / 2.0 + x1 / 2.0; 265 double z0 = zAxis.getRange().value(zfrac0); 266 double z1 = zAxis.getRange().value(zfrac1); 267 double zm = z0 / 2.0 + z1 / 2.0; 268 double y00 = this.function.getValue(x0, z0); 269 double y01 = this.function.getValue(x0, z1); 270 double y10 = this.function.getValue(x1, z0); 271 double y11 = this.function.getValue(x1, z1); 272 double ymm = this.function.getValue(xm, zm); 273 274 double wx0 = xAxis.translateToWorld(x0, xlen) + xOffset; 275 double wx1 = xAxis.translateToWorld(x1, xlen) + xOffset; 276 double wy00 = yAxis.translateToWorld(y00, ylen) + yOffset; 277 double wy01 = yAxis.translateToWorld(y01, ylen) + yOffset; 278 double wy10 = yAxis.translateToWorld(y10, ylen) + yOffset; 279 double wy11 = yAxis.translateToWorld(y11, ylen) + yOffset; 280 double wz0 = zAxis.translateToWorld(z0, zlen) + zOffset; 281 double wz1 = zAxis.translateToWorld(z1, zlen) + zOffset; 282 283 Color color = this.colorScale.valueToColor(ymm); 284 Object3D obj = new Object3D(color, this.drawFaceOutlines); 285 List<Point3D> pts1 = facePoints1(wx0, wx1, wz0, wz1, wy00, wy01, 286 wy11, yRange); 287 int count1 = pts1.size(); 288 for (Point3D pt : pts1) { 289 obj.addVertex(pt); 290 } 291 if (count1 == 3) { 292 obj.addDoubleSidedFace(new int[] {0, 1, 2}); 293 } else if (count1 == 4) { 294 obj.addDoubleSidedFace(new int[] {0, 1, 2, 3}); 295 } else if (count1 == 5) { 296 obj.addDoubleSidedFace(new int[] {0, 1, 2, 3, 4}); 297 } 298 List<Point3D> pts2 = facePoints2(wx0, wx1, wz0, wz1, wy00, wy11, 299 wy10, yRange); 300 int count2 = pts2.size(); 301 for (Point3D pt : pts2) { 302 obj.addVertex(pt); 303 } 304 if (count2 == 3) { 305 obj.addDoubleSidedFace(new int[] {count1, count1 + 1, 306 count1 + 2}); 307 } else if (count2 == 4) { 308 obj.addDoubleSidedFace(new int[] {count1, count1 + 1, 309 count1 + 2, count1 + 3}); 310 } else if (count2 == 5) { 311 obj.addDoubleSidedFace(new int[] {count1, count1 + 1, 312 count1 + 2, count1 + 3, count1 + 4}); 313 } 314 world.add(obj); 315 } 316 317 } 318 } 319 320 private Point3D intersectPoint(double x0, double y0, double z0, double x1, 321 double y1, double z1, double yy) { 322 double p = (yy - y0) / (y1 - y0); 323 double x = x0 + p * (x1 - x0); 324 double y = y0 + p * (y1 - y0); 325 double z = z0 + p * (z1 - z0); 326 return new Point3D(x, y, z); 327 } 328 329 private List<Point3D> facePoints1(double x0, double x1, double z0, 330 double z1, double y00, double y01, double y11, Range yRange) { 331 332 List<Point3D> pts = new ArrayList<>(4); 333 double ymin = yRange.getMin(); 334 double ymax = yRange.getMax(); 335 336 // handle y00 337 if (y00 > yRange.getMax()) { 338 if (yRange.contains(y01)) { 339 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax)); 340 } else if (y01 < yRange.getMin()) { 341 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax)); 342 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin)); 343 } 344 } else if (yRange.contains(y00)) { 345 pts.add(new Point3D(x0, y00, z0)); 346 if (y01 > yRange.getMax()) { 347 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax)); 348 } else if (y01 < yRange.getMin()) { 349 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin)); 350 } 351 } else { // below the range 352 if (yRange.contains(y01)) { 353 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin)); 354 } else if (y01 > yRange.getMax()) { 355 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin)); 356 pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax)); 357 } 358 } 359 360 // handle y01 361 if (y01 > yRange.getMax()) { 362 if (yRange.contains(y11)) { 363 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax)); 364 } else if (y11 < yRange.getMin()) { 365 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax)); 366 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin)); 367 } 368 } else if (yRange.contains(y01)) { 369 pts.add(new Point3D(x0, y01, z1)); 370 if (y11 > yRange.getMax()) { 371 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax)); 372 } else if (y11 < yRange.getMin()) { 373 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin)); 374 } 375 } else { 376 if (y11 > yRange.getMax()) { 377 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin)); 378 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax)); 379 } else if (yRange.contains(y11)) { 380 pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin)); 381 } 382 } 383 384 // handle y11 385 if (y11 > yRange.getMax()) { 386 if (yRange.contains(y00)) { 387 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax)); 388 } else if (y00 < yRange.getMin()) { 389 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax)); 390 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin)); 391 } 392 } else if (yRange.contains(y11)) { 393 pts.add(new Point3D(x1, y11, z1)); 394 if (y00 > yRange.getMax()) { 395 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax)); 396 } else if (y00 < yRange.getMin()) { 397 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin)); 398 } 399 } else { 400 if (y00 > yRange.getMax()) { 401 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin)); 402 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax)); 403 } else if (yRange.contains(y00)) { 404 pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin)); 405 } 406 } 407 return pts; 408 } 409 410 private List<Point3D> facePoints2(double x0, double x1, double z0, 411 double z1, double y00, double y11, double y10, Range yRange) { 412 413 List<Point3D> pts = new ArrayList<>(4); 414 double ymin = yRange.getMin(); 415 double ymax = yRange.getMax(); 416 // handle y00 417 if (y00 > yRange.getMax()) { 418 if (yRange.contains(y11)) { 419 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax)); 420 } else if (y11 < yRange.getMin()) { 421 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax)); 422 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin)); 423 } 424 } else if (yRange.contains(y00)) { 425 pts.add(new Point3D(x0, y00, z0)); 426 if (y11 > yRange.getMax()) { 427 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax)); 428 } else if (y11 < yRange.getMin()) { 429 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin)); 430 } 431 } else { // below the range 432 if (yRange.contains(y11)) { 433 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin)); 434 } else if (y11 > yRange.getMax()) { 435 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin)); 436 pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax)); 437 } 438 } 439 440 // handle y11 441 if (y11 > yRange.getMax()) { 442 if (yRange.contains(y10)) { 443 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax)); 444 } else if (y10 < yRange.getMin()) { 445 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax)); 446 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin)); 447 } 448 } else if (yRange.contains(y11)) { 449 pts.add(new Point3D(x1, y11, z1)); 450 if (y10 > yRange.getMax()) { 451 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax)); 452 } else if (y10 < yRange.getMin()) { 453 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin)); 454 } 455 } else { 456 if (y10 > yRange.getMax()) { 457 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin)); 458 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax)); 459 } else if (yRange.contains(y10)) { 460 pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin)); 461 } 462 } 463 464 // handle y10 465 if (y10 > yRange.getMax()) { 466 if (yRange.contains(y00)) { 467 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax)); 468 } else if (y00 < yRange.getMin()) { 469 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax)); 470 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin)); 471 } 472 } else if (yRange.contains(y10)) { 473 pts.add(new Point3D(x1, y10, z0)); 474 if (y00 > yRange.getMax()) { 475 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax)); 476 } else if (y00 < yRange.getMin()) { 477 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin)); 478 } 479 } else { 480 if (y00 > yRange.getMax()) { 481 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin)); 482 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax)); 483 } else if (yRange.contains(y00)) { 484 pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin)); 485 } 486 } 487 488 return pts; 489 } 490 491 /** 492 * Throws an {@code UnsupportedOperationException} because this 493 * renderer does not support per-item rendering. 494 * 495 * @param dataset the dataset ({@code null} not permitted). 496 * @param series the series index. 497 * @param item the item index. 498 * @param world the world ({@code null} not permitted). 499 * @param dimensions the dimensions ({@code null} not permitted). 500 * @param xOffset the x-offset. 501 * @param yOffset the y-offset. 502 * @param zOffset the z-offset. 503 */ 504 @Override 505 public void composeItem(XYZDataset dataset, int series, int item, 506 World world, Dimension3D dimensions, double xOffset, 507 double yOffset, double zOffset) { 508 throw new UnsupportedOperationException( 509 "Not supported by this renderer."); 510 } 511 512 /** 513 * Returns the current range for the x-axis - the method is overridden 514 * because this renderer does not use a dataset (it samples and plots a 515 * function directly). 516 * 517 * @param dataset the dataset (ignored). 518 * 519 * @return The x-range (never {@code null}). 520 */ 521 @Override 522 public Range findXRange(XYZDataset dataset) { 523 return getPlot().getXAxis().getRange(); 524 } 525 526 /** 527 * Returns the range that the renderer requires on the y-axis to display 528 * all the data in the function. 529 * 530 * @param dataset the dataset (ignored). 531 * 532 * @return The range. 533 */ 534 @Override 535 public Range findYRange(XYZDataset dataset) { 536 return Function3DUtils.findYRange(this.function, 537 getPlot().getXAxis().getRange(), 538 getPlot().getZAxis().getRange(), 539 this.xSamples, this.zSamples, true); 540 } 541 542 /** 543 * Returns the current range for the z-axis - the method is overridden 544 * because this renderer does not use a dataset (it samples and plots a 545 * function directly). 546 * 547 * @param dataset the dataset (ignored). 548 * 549 * @return The z-range (never {@code null}). 550 */ 551 @Override 552 public Range findZRange(XYZDataset dataset) { 553 return getPlot().getZAxis().getRange(); 554 } 555 556 /** 557 * Tests this renderer for equality with an arbitrary object. 558 * 559 * @param obj the object ({@code null} not permitted). 560 * 561 * @return A boolean. 562 */ 563 @Override 564 public boolean equals(Object obj) { 565 if (obj == this) { 566 return true; 567 } 568 if (!(obj instanceof SurfaceRenderer)) { 569 return false; 570 } 571 SurfaceRenderer that = (SurfaceRenderer) obj; 572 if (!this.function.equals(that.function)) { 573 return false; 574 } 575 if (this.xSamples != that.xSamples) { 576 return false; 577 } 578 if (this.zSamples != that.zSamples) { 579 return false; 580 } 581 if (!this.colorScale.equals(that.colorScale)) { 582 return false; 583 } 584 if (this.drawFaceOutlines != that.drawFaceOutlines) { 585 return false; 586 } 587 return super.equals(obj); 588 } 589}