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.data; 034 035import java.io.IOException; 036import java.io.StringWriter; 037import java.io.Writer; 038import java.io.Reader; 039import java.io.StringReader; 040import java.util.ArrayList; 041import java.util.LinkedHashMap; 042import java.util.List; 043import java.util.Map; 044import org.jfree.chart3d.util.json.JSONValue; 045import org.jfree.chart3d.util.json.parser.JSONParser; 046import org.jfree.chart3d.util.json.parser.ParseException; 047import org.jfree.chart3d.internal.Args; 048import org.jfree.chart3d.util.json.parser.ContainerFactory; 049import org.jfree.chart3d.data.category.StandardCategoryDataset3D; 050import org.jfree.chart3d.data.xyz.XYZDataset; 051import org.jfree.chart3d.data.xyz.XYZSeries; 052import org.jfree.chart3d.data.xyz.XYZSeriesCollection; 053 054/** 055 * Utility methods for interchange between datasets ({@link KeyedValues}, 056 * {@link KeyedValues3D} and {@link XYZDataset}) and JSON format strings. 057 * 058 * @since 1.3 059 */ 060public class JSONUtils { 061 062 /** 063 * Parses the supplied JSON string into a {@link KeyedValues} instance. 064 * <br><br> 065 * Implementation note: this method returns an instance of 066 * {@link StandardPieDataset3D}). 067 * 068 * @param json the JSON string ({@code null} not permitted). 069 * 070 * @return A {@link KeyedValues} instance. 071 */ 072 public static KeyedValues<String, Number> readKeyedValues(String json) { 073 Args.nullNotPermitted(json, "json"); 074 StringReader in = new StringReader(json); 075 KeyedValues<String, Number> result; 076 try { 077 result = readKeyedValues(in); 078 } catch (IOException ex) { 079 // not for StringReader 080 result = null; 081 } 082 return result; 083 } 084 085 /** 086 * Parses characters from the supplied reader and returns the corresponding 087 * {@link KeyedValues} instance. 088 * <br><br> 089 * Implementation note: this method returns an instance of 090 * {@link StandardPieDataset3D}). 091 * 092 * @param reader the reader ({@code null} not permitted). 093 * 094 * @return A {@code KeyedValues} instance. 095 * 096 * @throws IOException if there is an I/O problem. 097 */ 098 public static KeyedValues<String, Number> readKeyedValues( 099 Reader reader) throws IOException { 100 Args.nullNotPermitted(reader, "reader"); 101 try { 102 JSONParser parser = new JSONParser(); 103 // parse with custom containers (to preserve item order) 104 List list = (List) parser.parse(reader, createContainerFactory()); 105 StandardPieDataset3D<String> result = new StandardPieDataset3D<>(); 106 for (Object item : list) { 107 List itemAsList = (List) item; 108 result.add(itemAsList.get(0).toString(), (Number) itemAsList.get(1)); 109 } 110 return result; 111 } catch (ParseException ex) { 112 throw new RuntimeException(ex); 113 } 114 } 115 116 /** 117 * Returns a string containing the data in JSON format. The format is 118 * an array of arrays, where each sub-array represents one data value. 119 * The sub-array should contain two items, first the item key as a string 120 * and second the item value as a number. For example: 121 * {@code [["Key A", 1.0], ["Key B", 2.0]]} 122 * <br><br> 123 * Note that this method can be used with instances of {@link PieDataset3D}. 124 * 125 * @param data the data ({@code null} not permitted). 126 * 127 * @return A string in JSON format. 128 */ 129 @SuppressWarnings("unchecked") 130 public static String writeKeyedValues(KeyedValues data) { 131 Args.nullNotPermitted(data, "data"); 132 StringWriter sw = new StringWriter(); 133 try { 134 writeKeyedValues(data, sw); 135 } catch (IOException ex) { 136 throw new RuntimeException(ex); 137 } 138 return sw.toString(); 139 } 140 141 /** 142 * Writes the data in JSON format to the supplied writer. 143 * <br><br> 144 * Note that this method can be used with instances of {@link PieDataset3D}. 145 * 146 * @param data the data ({@code null} not permitted). 147 * @param writer the writer ({@code null} not permitted). 148 * 149 * @throws IOException if there is an I/O problem. 150 */ 151 @SuppressWarnings("unchecked") 152 public static void writeKeyedValues(KeyedValues data, Writer writer) 153 throws IOException { 154 Args.nullNotPermitted(data, "data"); 155 Args.nullNotPermitted(writer, "writer"); 156 writer.write("["); 157 boolean first = true; 158 for (Object key : data.getKeys()) { 159 if (!first) { 160 writer.write(", "); 161 } else { 162 first = false; 163 } 164 writer.write("["); 165 writer.write(JSONValue.toJSONString(key.toString())); 166 writer.write(", "); 167 writer.write(JSONValue.toJSONString(data.getValue((Comparable) key))); 168 writer.write("]"); 169 } 170 writer.write("]"); 171 } 172 173 /** 174 * Reads a data table from a JSON format string. 175 * 176 * @param json the string ({@code null} not permitted). 177 * 178 * @return A data table. 179 */ 180 @SuppressWarnings("unchecked") 181 public static KeyedValues2D<String, String, Number> 182 readKeyedValues2D(String json) { 183 Args.nullNotPermitted(json, "json"); 184 StringReader in = new StringReader(json); 185 KeyedValues2D<String, String, Number> result; 186 try { 187 result = readKeyedValues2D(in); 188 } catch (IOException ex) { 189 // not for StringReader 190 result = null; 191 } 192 return result; 193 } 194 195 /** 196 * Reads a data table from a JSON format string coming from the specified 197 * reader. 198 * 199 * @param reader the reader ({@code null} not permitted). 200 * 201 * @return A data table. 202 * 203 * @throws java.io.IOException if there is an I/O problem. 204 */ 205 @SuppressWarnings("unchecked") 206 public static KeyedValues2D<String, String, Number> 207 readKeyedValues2D(Reader reader) throws IOException { 208 209 JSONParser parser = new JSONParser(); 210 try { 211 Map map = (Map) parser.parse(reader, createContainerFactory()); 212 DefaultKeyedValues2D<String, String, Number> result 213 = new DefaultKeyedValues2D<>(); 214 if (map.isEmpty()) { 215 return result; 216 } 217 218 // read the keys 219 Object keysObj = map.get("columnKeys"); 220 List<String> keys = null; 221 if (keysObj instanceof List) { 222 keys = (List<String>) keysObj; 223 } else { 224 if (keysObj == null) { 225 throw new RuntimeException("No 'columnKeys' defined."); 226 } else { 227 throw new RuntimeException("Please check the 'columnKeys', " 228 + "the format does not parse to a list."); 229 } 230 } 231 232 Object dataObj = map.get("rows"); 233 if (dataObj instanceof List) { 234 List<String> rowList = (List<String>) dataObj; 235 // each entry in the map has the row key and an array of 236 // values (the length should match the list of keys above 237 for (Object rowObj : rowList) { 238 processRow(rowObj, keys, result); 239 } 240 } else { // the 'data' entry is not parsing to a list 241 if (dataObj == null) { 242 throw new RuntimeException("No 'rows' section defined."); 243 } else { 244 throw new RuntimeException("Please check the 'rows' " 245 + "entry, the format does not parse to a list of " 246 + "rows."); 247 } 248 } 249 return result; 250 } catch (ParseException ex) { 251 throw new RuntimeException(ex); 252 } 253 } 254 255 /** 256 * Processes an entry for one row in a {@link KeyedValues2D}. 257 * 258 * @param rowObj the series object. 259 * @param columnKeys the required column keys. 260 * @param dataset the dataset. 261 */ 262 @SuppressWarnings("unchecked") 263 static void processRow(Object rowObj, List<String> columnKeys, 264 DefaultKeyedValues2D dataset) { 265 266 if (!(rowObj instanceof List)) { 267 throw new RuntimeException("Check the 'data' section it contains " 268 + "a row that does not parse to a list."); 269 } 270 271 // we expect the row data object to be an array containing the 272 // rowKey and rowValueArray entries, where rowValueArray 273 // should have the same number of entries as the columnKeys 274 List rowList = (List) rowObj; 275 Object rowKey = rowList.get(0); 276 Object rowDataObj = rowList.get(1); 277 if (!(rowDataObj instanceof List)) { 278 throw new RuntimeException("Please check the row entry for " 279 + rowKey + " because it is not parsing to a list (of " 280 + "rowKey and rowDataValues items."); 281 } 282 List<?> rowData = (List<?>) rowDataObj; 283 if (rowData.size() != columnKeys.size()) { 284 throw new RuntimeException("The values list for series " 285 + rowKey + " does not contain the correct number of " 286 + "entries to match the columnKeys."); 287 } 288 289 for (int c = 0; c < rowData.size(); c++) { 290 Object columnKey = columnKeys.get(c); 291 dataset.setValue(objToDouble(rowData.get(c)), 292 rowKey.toString(), columnKey.toString()); 293 } 294 } 295 296 /** 297 * Writes a data table to a string in JSON format. 298 * 299 * @param data the data ({@code null} not permitted). 300 * 301 * @return The string. 302 */ 303 public static String writeKeyedValues2D(KeyedValues2D data) { 304 Args.nullNotPermitted(data, "data"); 305 StringWriter sw = new StringWriter(); 306 try { 307 writeKeyedValues2D(data, sw); 308 } catch (IOException ex) { 309 throw new RuntimeException(ex); 310 } 311 return sw.toString(); 312 } 313 314 /** 315 * Writes the data in JSON format to the supplied writer. 316 * 317 * @param data the data ({@code null} not permitted). 318 * @param writer the writer ({@code null} not permitted). 319 * 320 * @throws IOException if there is an I/O problem. 321 */ 322 @SuppressWarnings("unchecked") 323 public static void writeKeyedValues2D(KeyedValues2D data, Writer writer) 324 throws IOException { 325 Args.nullNotPermitted(data, "data"); 326 Args.nullNotPermitted(writer, "writer"); 327 List<Comparable> columnKeys = data.getColumnKeys(); 328 List<Comparable> rowKeys = data.getRowKeys(); 329 writer.write("{"); 330 if (!columnKeys.isEmpty()) { 331 writer.write("\"columnKeys\": ["); 332 boolean first = true; 333 for (Comparable columnKey : columnKeys) { 334 if (!first) { 335 writer.write(", "); 336 } else { 337 first = false; 338 } 339 writer.write(JSONValue.toJSONString(columnKey.toString())); 340 } 341 writer.write("]"); 342 } 343 if (!rowKeys.isEmpty()) { 344 writer.write(", \"rows\": ["); 345 boolean firstRow = true; 346 for (Comparable rowKey : rowKeys) { 347 if (!firstRow) { 348 writer.write(", ["); 349 } else { 350 writer.write("["); 351 firstRow = false; 352 } 353 // write the row data 354 writer.write(JSONValue.toJSONString(rowKey.toString())); 355 writer.write(", ["); 356 boolean first = true; 357 for (Comparable columnKey : columnKeys) { 358 if (!first) { 359 writer.write(", "); 360 } else { 361 first = false; 362 } 363 writer.write(JSONValue.toJSONString(data.getValue(rowKey, 364 columnKey))); 365 } 366 writer.write("]]"); 367 } 368 writer.write("]"); 369 } 370 writer.write("}"); 371 } 372 373 /** 374 * Parses the supplied string and (if possible) creates a 375 * {@link KeyedValues3D} instance. 376 * 377 * @param json the JSON string ({@code null} not permitted). 378 * 379 * @return A {@code KeyedValues3D} instance. 380 */ 381 public static KeyedValues3D<String, String, String, Number> 382 readKeyedValues3D(String json) { 383 StringReader in = new StringReader(json); 384 KeyedValues3D<String, String, String, Number> result; 385 try { 386 result = readKeyedValues3D(in); 387 } catch (IOException ex) { 388 // not for StringReader 389 result = null; 390 } 391 return result; 392 } 393 394 /** 395 * Parses character data from the reader and (if possible) creates a 396 * {@link KeyedValues3D} instance. This method will read back the data 397 * written by {@link JSONUtils#writeKeyedValues3D( 398 * org.jfree.chart3d.data.KeyedValues3D, java.io.Writer) }. 399 * 400 * @param reader the reader ({@code null} not permitted). 401 * 402 * @return A {@code KeyedValues3D} instance. 403 * 404 * @throws IOException if there is an I/O problem. 405 */ 406 @SuppressWarnings("unchecked") 407 public static KeyedValues3D<String, String, String, Number> 408 readKeyedValues3D(Reader reader) throws IOException { 409 JSONParser parser = new JSONParser(); 410 try { 411 Map map = (Map) parser.parse(reader, createContainerFactory()); 412 StandardCategoryDataset3D result = new StandardCategoryDataset3D(); 413 if (map.isEmpty()) { 414 return result; 415 } 416 417 // read the row keys, we'll use these to validate the row keys 418 // supplied with the data 419 Object rowKeysObj = map.get("rowKeys"); 420 List<String> rowKeys; 421 if (rowKeysObj instanceof List) { 422 rowKeys = (List<String>) rowKeysObj; 423 } else { 424 if (rowKeysObj == null) { 425 throw new RuntimeException("No 'rowKeys' defined."); 426 } else { 427 throw new RuntimeException("Please check the 'rowKeys', " 428 + "the format does not parse to a list."); 429 } 430 } 431 432 // read the column keys, the data is provided later in rows that 433 // should have the same number of entries as the columnKeys list 434 Object columnKeysObj = map.get("columnKeys"); 435 List<String> columnKeys; 436 if (columnKeysObj instanceof List) { 437 columnKeys = (List<String>) columnKeysObj; 438 } else { 439 if (columnKeysObj == null) { 440 throw new RuntimeException("No 'columnKeys' defined."); 441 } else { 442 throw new RuntimeException("Please check the 'columnKeys', " 443 + "the format does not parse to a list."); 444 } 445 } 446 447 // the data object should be a list of data series 448 Object dataObj = map.get("data"); 449 if (dataObj instanceof List) { 450 List<String> seriesList = (List<String>) dataObj; 451 // each entry in the map has the series name as the key, and 452 // the value is a map of row data (rowKey, list of values) 453 for (Object seriesObj : seriesList) { 454 processSeries(seriesObj, rowKeys, columnKeys, result); 455 } 456 } else { // the 'data' entry is not parsing to a list 457 if (dataObj == null) { 458 throw new RuntimeException("No 'data' section defined."); 459 } else { 460 throw new RuntimeException("Please check the 'data' " 461 + "entry, the format does not parse to a list of " 462 + "series."); 463 } 464 } 465 return result; 466 } catch (ParseException ex) { 467 throw new RuntimeException(ex); 468 } 469 } 470 471 /** 472 * Processes an entry for one series. 473 * 474 * @param seriesObj the series object. 475 * @param rowKeys the expected row keys. 476 * @param columnKeys the required column keys. 477 */ 478 static <R extends Comparable<R>, C extends Comparable<C>> 479 void processSeries(Object seriesObj, List<R> rowKeys, 480 List<C> columnKeys, 481 StandardCategoryDataset3D<String, String, String> dataset) { 482 483 if (!(seriesObj instanceof Map)) { 484 throw new RuntimeException("Check the 'data' section it contains " 485 + "a series that does not parse to a map."); 486 } 487 488 // we expect the series data object to be a map of 489 // rowKey ==> rowValueArray entries, where rowValueArray 490 // should have the same number of entries as the columnKeys 491 Map seriesMap = (Map) seriesObj; 492 Object seriesKey = seriesMap.get("seriesKey"); 493 Object seriesRowsObj = seriesMap.get("rows"); 494 if (!(seriesRowsObj instanceof Map)) { 495 throw new RuntimeException("Please check the series entry for " 496 + seriesKey + " because it is not parsing to a map (of " 497 + "rowKey -> rowDataValues items."); 498 } 499 Map<?, ?> seriesData = (Map<?, ?>) seriesRowsObj; 500 for (Object rowKey : seriesData.keySet()) { 501 if (!rowKeys.contains(rowKey)) { 502 throw new RuntimeException("The row key " + rowKey + " is not " 503 + "listed in the rowKeys entry."); 504 } 505 Object rowValuesObj = seriesData.get(rowKey); 506 if (!(rowValuesObj instanceof List<?>)) { 507 throw new RuntimeException("Please check the entry for series " 508 + seriesKey + " and row " + rowKey + " because it " 509 + "does not parse to a list of values."); 510 } 511 List<?> rowValues = (List<?>) rowValuesObj; 512 if (rowValues.size() != columnKeys.size()) { 513 throw new RuntimeException("The values list for series " 514 + seriesKey + " and row " + rowKey + " does not " 515 + "contain the correct number of entries to match " 516 + "the columnKeys."); 517 } 518 for (int c = 0; c < rowValues.size(); c++) { 519 Object columnKey = columnKeys.get(c); 520 dataset.addValue(objToDouble(rowValues.get(c)), 521 seriesKey.toString(), rowKey.toString(), 522 columnKey.toString()); 523 } 524 } 525 } 526 527 /** 528 * Returns a string containing the data in JSON format. 529 * 530 * @param dataset the data ({@code null} not permitted). 531 * 532 * @return A string in JSON format. 533 */ 534 public static String writeKeyedValues3D(KeyedValues3D dataset) { 535 Args.nullNotPermitted(dataset, "dataset"); 536 StringWriter sw = new StringWriter(); 537 try { 538 writeKeyedValues3D(dataset, sw); 539 } catch (IOException ex) { 540 throw new RuntimeException(ex); 541 } 542 return sw.toString(); 543 } 544 545 /** 546 * Writes the dataset in JSON format to the supplied writer. 547 * 548 * @param dataset the dataset ({@code null} not permitted). 549 * @param writer the writer ({@code null} not permitted). 550 * 551 * @throws IOException if there is an I/O problem. 552 */ 553 @SuppressWarnings("unchecked") 554 public static void writeKeyedValues3D(KeyedValues3D dataset, Writer writer) 555 throws IOException { 556 Args.nullNotPermitted(dataset, "dataset"); 557 Args.nullNotPermitted(writer, "writer"); 558 559 writer.write("{"); 560 if (!dataset.getColumnKeys().isEmpty()) { 561 writer.write("\"columnKeys\": ["); 562 boolean first = true; 563 for (Object key : dataset.getColumnKeys()) { 564 if (!first) { 565 writer.write(", "); 566 } else { 567 first = false; 568 } 569 writer.write(JSONValue.toJSONString(key.toString())); 570 } 571 writer.write("], "); 572 } 573 574 // write the row keys 575 if (!dataset.getRowKeys().isEmpty()) { 576 writer.write("\"rowKeys\": ["); 577 boolean first = true; 578 for (Object key : dataset.getRowKeys()) { 579 if (!first) { 580 writer.write(", "); 581 } else { 582 first = false; 583 } 584 writer.write(JSONValue.toJSONString(key.toString())); 585 } 586 writer.write("], "); 587 } 588 589 // write the data which is zero, one or many data series 590 // a data series has a 'key' and a 'rows' attribute 591 // the 'rows' attribute is a Map from 'rowKey' -> array of data values 592 if (dataset.getSeriesCount() != 0) { 593 writer.write("\"series\": ["); 594 boolean first = true; 595 for (Object seriesKey : dataset.getSeriesKeys()) { 596 if (!first) { 597 writer.write(", "); 598 } else { 599 first = false; 600 } 601 writer.write("{\"seriesKey\": "); 602 writer.write(JSONValue.toJSONString(seriesKey.toString())); 603 writer.write(", \"rows\": ["); 604 605 boolean firstRow = true; 606 for (Object rowKey : dataset.getRowKeys()) { 607 if (countForRowInSeries(dataset, (Comparable) seriesKey, 608 (Comparable) rowKey) > 0) { 609 if (!firstRow) { 610 writer.write(", ["); 611 } else { 612 writer.write("["); 613 firstRow = false; 614 } 615 // write the row values 616 writer.write(JSONValue.toJSONString(rowKey.toString()) 617 + ", ["); 618 for (int c = 0; c < dataset.getColumnCount(); c++) { 619 Object columnKey = dataset.getColumnKey(c); 620 if (c != 0) { 621 writer.write(", "); 622 } 623 writer.write(JSONValue.toJSONString( 624 dataset.getValue((Comparable) seriesKey, 625 (Comparable) rowKey, 626 (Comparable) columnKey))); 627 } 628 writer.write("]]"); 629 } 630 } 631 writer.write("]}"); 632 } 633 writer.write("]"); 634 } 635 writer.write("}"); 636 } 637 638 /** 639 * Returns the number of non-{@code null} entries for the specified 640 * series and row. 641 * 642 * @param data the dataset ({@code null} not permitted). 643 * @param seriesKey the series key ({@code null} not permitted). 644 * @param rowKey the row key ({@code null} not permitted). 645 * 646 * @return The count. 647 */ 648 @SuppressWarnings("unchecked") 649 private static int countForRowInSeries(KeyedValues3D data, 650 Comparable seriesKey, Comparable rowKey) { 651 Args.nullNotPermitted(data, "data"); 652 Args.nullNotPermitted(seriesKey, "seriesKey"); 653 Args.nullNotPermitted(rowKey, "rowKey"); 654 int seriesIndex = data.getSeriesIndex(seriesKey); 655 if (seriesIndex < 0) { 656 throw new IllegalArgumentException("Series not found: " 657 + seriesKey); 658 } 659 int rowIndex = data.getRowIndex(rowKey); 660 if (rowIndex < 0) { 661 throw new IllegalArgumentException("Row not found: " + rowKey); 662 } 663 int count = 0; 664 for (int c = 0; c < data.getColumnCount(); c++) { 665 Object n = data.getValue(seriesIndex, rowIndex, c); 666 if (n != null) { 667 count++; 668 } 669 } 670 return count; 671 } 672 673 /** 674 * Parses the string and (if possible) creates an {XYZDataset} instance 675 * that represents the data. This method will read back the data that 676 * is written by 677 * {@link #writeXYZDataset(org.jfree.chart3d.data.xyz.XYZDataset)}. 678 * 679 * @param json a JSON formatted string ({@code null} not permitted). 680 * 681 * @return A dataset. 682 * 683 * @see #writeXYZDataset(org.jfree.chart3d.data.xyz.XYZDataset) 684 */ 685 public static XYZDataset<String> readXYZDataset(String json) { 686 Args.nullNotPermitted(json, "json"); 687 StringReader in = new StringReader(json); 688 XYZDataset<String> result; 689 try { 690 result = readXYZDataset(in); 691 } catch (IOException ex) { 692 // not for StringReader 693 result = null; 694 } 695 return result; 696 } 697 698 /** 699 * Parses character data from the reader and (if possible) creates an 700 * {XYZDataset} instance that represents the data. 701 * 702 * @param reader a reader ({@code null} not permitted). 703 * 704 * @return A dataset. 705 * 706 * @throws IOException if there is an I/O problem. 707 */ 708 @SuppressWarnings("unchecked") 709 public static XYZDataset<String> readXYZDataset(Reader reader) throws IOException { 710 JSONParser parser = new JSONParser(); 711 XYZSeriesCollection<String> result = new XYZSeriesCollection<>(); 712 try { 713 List<?> list = (List<?>) parser.parse(reader, 714 createContainerFactory()); 715 // each entry in the array should be a series array (where the 716 // first item is the series name and the next value is an array 717 // (of arrays of length 3) containing the data items 718 for (Object seriesArray : list) { 719 if (seriesArray instanceof List) { 720 List<?> seriesList = (List<?>) seriesArray; 721 XYZSeries series = createSeries(seriesList); 722 result.add(series); 723 } else { 724 throw new RuntimeException( 725 "Input for a series did not parse to a list."); 726 } 727 } 728 } catch (ParseException ex) { 729 throw new RuntimeException(ex); 730 } 731 return result; 732 } 733 734 /** 735 * Returns a string containing the dataset in JSON format. 736 * 737 * @param dataset the dataset ({@code null} not permitted). 738 * 739 * @return A string in JSON format. 740 */ 741 public static String writeXYZDataset(XYZDataset dataset) { 742 StringWriter sw = new StringWriter(); 743 try { 744 writeXYZDataset(dataset, sw); 745 } catch (IOException ex) { 746 throw new RuntimeException(ex); 747 } 748 return sw.toString(); 749 } 750 751 /** 752 * Writes the dataset in JSON format to the supplied writer. 753 * 754 * @param dataset the data ({@code null} not permitted). 755 * @param writer the writer ({@code null} not permitted). 756 * 757 * @throws IOException if there is an I/O problem. 758 */ 759 @SuppressWarnings("unchecked") 760 public static void writeXYZDataset(XYZDataset dataset, Writer writer) 761 throws IOException { 762 writer.write("["); 763 boolean first = true; 764 for (Object seriesKey : dataset.getSeriesKeys()) { 765 if (!first) { 766 writer.write(", ["); 767 } else { 768 writer.write("["); 769 first = false; 770 } 771 writer.write(JSONValue.toJSONString(seriesKey.toString())); 772 writer.write(", ["); 773 int seriesIndex = dataset.getSeriesIndex((Comparable) seriesKey); 774 int itemCount = dataset.getItemCount(seriesIndex); 775 for (int i = 0; i < itemCount; i++) { 776 if (i != 0) { 777 writer.write(", "); 778 } 779 writer.write("["); 780 writer.write(JSONValue.toJSONString( 781 dataset.getX(seriesIndex, i))); 782 writer.write(", "); 783 writer.write(JSONValue.toJSONString( 784 dataset.getY(seriesIndex, i))); 785 writer.write(", "); 786 writer.write(JSONValue.toJSONString( 787 dataset.getZ(seriesIndex, i))); 788 writer.write("]"); 789 } 790 writer.write("]]"); 791 } 792 writer.write("]"); 793 } 794 795 /** 796 * Converts an arbitrary object to a double. 797 * 798 * @param obj an object ({@code null} permitted). 799 * 800 * @return A double primitive (possibly Double.NaN). 801 */ 802 private static double objToDouble(Object obj) { 803 if (obj == null) { 804 return Double.NaN; 805 } 806 if (obj instanceof Number) { 807 return ((Number) obj).doubleValue(); 808 } 809 double result = Double.NaN; 810 try { 811 result = Double.parseDouble(obj.toString()); 812 } catch (Exception e) { 813 814 } 815 return result; 816 } 817 818 /** 819 * Creates an {@link XYZSeries} from the supplied list. The list is 820 * coming from the JSON parser and should contain the series name as the 821 * first item, and a list of data items as the second item. The list of 822 * data items should be a list of lists ( 823 * 824 * @param sArray the series array. 825 * 826 * @return A data series. 827 */ 828 @SuppressWarnings("unchecked") 829 private static XYZSeries createSeries(List<?> sArray) { 830 Comparable<?> key = (Comparable<?>) sArray.get(0); 831 List<?> dataItems = (List<?>) sArray.get(1); 832 XYZSeries series = new XYZSeries(key); 833 for (Object item : dataItems) { 834 if (item instanceof List<?>) { 835 List<?> xyz = (List<?>) item; 836 if (xyz.size() != 3) { 837 throw new RuntimeException( 838 "A data item should contain three numbers, " 839 + "but we have " + xyz); 840 } 841 double x = objToDouble(xyz.get(0)); 842 double y = objToDouble(xyz.get(1)); 843 double z = objToDouble(xyz.get(2)); 844 series.add(x, y, z); 845 846 } else { 847 throw new RuntimeException( 848 "Expecting a data item (x, y, z) for series " + key 849 + " but found " + item + "."); 850 } 851 } 852 return series; 853 } 854 855 /** 856 * Returns a custom container factory for the JSON parser. We create this 857 * so that the collections respect the order of elements. 858 * 859 * @return The container factory. 860 */ 861 private static ContainerFactory createContainerFactory() { 862 return new ContainerFactory() { 863 @Override 864 public Map createObjectContainer() { 865 return new LinkedHashMap(); 866 } 867 868 @Override 869 public List creatArrayContainer() { 870 return new ArrayList(); 871 } 872 }; 873 } 874 875}