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