From 583ecf2df556c0bb3ae5468a63d9231b3ef56f58 Mon Sep 17 00:00:00 2001 From: ArghKevin Date: Mon, 20 May 2024 05:42:37 -0700 Subject: Weeks 5 and 6. --- DejaVuSansCondensed.ttf | Bin 0 -> 680264 bytes LICENSE | 210 +++++++++++++++++++++++++++++++++++++++++++++--- README.md | 15 ++++ b | 50 ++++++++++++ src/CSVReader.java | 35 ++++++-- src/ComparisonView.java | 107 +++++++++++++++--------- src/FontFamily.java | 93 +++++++++++++++++++-- src/JSONReader.java | 96 +++++++++++++++++----- src/LineGraph.java | 197 +++++++++++++++++++++++++++++++++++++++++++-- src/Reader.java | 2 +- week5.png | Bin 0 -> 10253 bytes week6.png | Bin 0 -> 59354 bytes 12 files changed, 717 insertions(+), 88 deletions(-) create mode 100644 DejaVuSansCondensed.ttf create mode 100644 b create mode 100644 week5.png create mode 100644 week6.png diff --git a/DejaVuSansCondensed.ttf b/DejaVuSansCondensed.ttf new file mode 100644 index 0000000..3259bc2 Binary files /dev/null and b/DejaVuSansCondensed.ttf differ diff --git a/LICENSE b/LICENSE index 7ae8865..12d7354 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,200 @@ -Permission to use, copy, modify, and/or distribute this software for -any purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL -WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE -FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY -DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN -AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The original software in this directory falls under the following license: + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +DejaVuSansCondensed.ttf falls under the following licenses: + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +TeX Gyre DJV Math +----------------- +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + +Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski +(on behalf of TeX users groups) are in public domain. + +Letters imported from Euler Fraktur from AMSfonts are (c) American +Mathematical Society (see below). +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera +is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation +files (the "Font Software"), to reproduce and distribute the Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, +and/or sell copies of the Font Software, and to permit persons to whom +the Font Software is furnished to do so, subject to the following +conditions: + +The above copyright and trademark notices and this permission notice +shall be +included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional +glyphs or characters may be added to the Fonts, only if the fonts are +renamed +to names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or +Font Software +that has been modified and is distributed under the "Bitstream Vera" +names. + +The Font Software may be sold as part of a larger software package but +no copy +of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, +SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN +ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR +INABILITY TO USE +THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. +Except as contained in this notice, the names of GNOME, the GNOME +Foundation, +and Bitstream Inc., shall not be used in advertising or otherwise to promote +the sale, use or other dealings in this Font Software without prior written +authorization from the GNOME Foundation or Bitstream Inc., respectively. +For further information, contact: fonts at gnome dot org. + +AMSFonts (v. 2.2) copyright + +The PostScript Type 1 implementation of the AMSFonts produced by and +previously distributed by Blue Sky Research and Y&Y, Inc. are now freely +available for general use. This has been accomplished through the +cooperation +of a consortium of scientific publishers with Blue Sky Research and Y&Y. +Members of this consortium include: + +Elsevier Science IBM Corporation Society for Industrial and Applied +Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) + +In order to assure the authenticity of these fonts, copyright will be +held by +the American Mathematical Society. This is not meant to restrict in any way +the legitimate use of the fonts, such as (but not limited to) electronic +distribution of documents containing these fonts, inclusion of these fonts +into other public domain or commercial font collections or computer +applications, use of the outline data to create derivative fonts and/or +faces, etc. However, the AMS does require that the AMS copyright notice be +removed from any derivative versions of the fonts which have been altered in +any way. In addition, to ensure the fidelity of TeX documents using Computer +Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, +has requested that any alterations which yield different font metrics be +given a different name. diff --git a/README.md b/README.md index 0087f37..96644cc 100644 --- a/README.md +++ b/README.md @@ -162,3 +162,18 @@ sets of data describing the Unicode subsets implemented by a font. Along with JS this fulfulls most of the same purpose. No disk space calculation, unfortunately. There are 9 days left. ![week4](week4.png) + +## Weeks 5 and 6: Updates +JSON parsing as it is needed is wholly implemented. +Each FontFamily is populated with all fields. +The popularity metrics are implemented as a HashMap. +The LineGraph has been implemented as a JPanel with a custom paintCompontent method. +Included with it is a calculation of the top 10 most popular typefaces. +This isn't quite done yet, because also being able to view graphs of the top typeface styles +would be preferrable. Further, there is not yet any option to expose the metadata of each font. +Also, graphing by different time metrics would be preferable. +The project is at the stage where there are pretty colors, however the utility is not yet fulfilled. +The project current fulfills all LOs except LO6. I'll be working on that next. +I have until the 25th. It's the 20th. 5 days left. +![week5](week5.png) +![week5](week5.png) diff --git a/b b/b new file mode 100644 index 0000000..51cd503 --- /dev/null +++ b/b @@ -0,0 +1,50 @@ +Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "java.util.HashMap.get(Object)" is null + at LineGraph$1.compare(LineGraph.java:60) + at LineGraph$1.compare(LineGraph.java:58) + at java.base/java.util.TimSort.binarySort(TimSort.java:296) + at java.base/java.util.TimSort.sort(TimSort.java:239) + at java.base/java.util.Arrays.sort(Arrays.java:1308) + at java.base/java.util.ArrayList.sort(ArrayList.java:1804) + at java.base/java.util.Collections.sort(Collections.java:178) + at LineGraph.sortYearly(LineGraph.java:58) + at LineGraph.monthly(LineGraph.java:76) + at LineGraph.paintComponent(LineGraph.java:54) + at java.desktop/javax.swing.JComponent.paint(JComponent.java:1128) + at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:961) + at java.desktop/javax.swing.JComponent.paint(JComponent.java:1137) + at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:961) + at java.desktop/javax.swing.JComponent.paint(JComponent.java:1137) + at java.desktop/javax.swing.JLayeredPane.paint(JLayeredPane.java:586) + at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:961) + at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5325) + at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBufferedImpl(RepaintManager.java:1656) + at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1631) + at java.desktop/javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1569) + at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1336) + at java.desktop/javax.swing.JComponent.paint(JComponent.java:1114) + at java.desktop/java.awt.GraphicsCallback$PaintCallback.run(GraphicsCallback.java:39) + at java.desktop/sun.awt.SunGraphicsCallback.runOneComponent(SunGraphicsCallback.java:75) + at java.desktop/sun.awt.SunGraphicsCallback.runComponents(SunGraphicsCallback.java:112) + at java.desktop/java.awt.Container.paint(Container.java:2005) + at java.desktop/java.awt.Window.paint(Window.java:3959) + at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:889) + at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:861) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87) + at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:861) + at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:834) + at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:784) + at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1897) + at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318) + at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773) + at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720) + at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87) + at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742) + at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203) + at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124) + at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113) + at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109) + at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) + at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90) diff --git a/src/CSVReader.java b/src/CSVReader.java index fa588ef..3666531 100644 --- a/src/CSVReader.java +++ b/src/CSVReader.java @@ -17,14 +17,15 @@ import java.util.Arrays; */ public class CSVReader extends Reader { - String[] header; // A CSV file has-a header. One line, multiple fields. - /* Array of Hash map from header to value for each - line past the header. */ - ArrayList> body; // A CSV file has-a body. Multiple lines, multiple fields. + private String[] header; // A CSV file has-a header. One line, multiple fields. + // Array of HashMaps. Headers are keys, values are on each line of the CSV following the header. + private ArrayList> body; // A CSV file has-a body. Multiple lines, multiple fields. public CSVReader(File file) { super(file); // Call parent constructor + /* Construct the body to be filled. */ body = new ArrayList>(); + /* Parse the contents of the file, store in body. */ parse(); } @@ -32,26 +33,46 @@ public class CSVReader extends Reader { * Parse CSV file. */ public void parse() { + /* Split the input on newlines. */ String[] lines = this.getContents().split("\n"); + /* Split the first line by commas, assign to header array. */ this.header = lines[0].split(","); - /* Iterate over all lines of the body. */ + /* Iterate over all lines of the body. + First line is header, count from 1 instead of 0. */ for (int i = 1; i < lines.length; i++) { + /* Some lines have commas within parentheses. + Angkor,"/South East Asian (Thai, Khmer, Lao)/Looped",100 + Remove everything within parentheses. We need to escape + not on a language level, but on a function call level. + Double-backslash for a literal backslash, which is then + used to escape the parentheses in the regex library. */ + lines[i] = lines[i].replaceAll("\\(.*\\)", ""); + + /* CSV, Comma-Separated Values. */ String[] fields = lines[i].split(","); + /* Allocate a new HashMap for the given line of the CSV. */ HashMap map = new HashMap(); - /* For each of the columns, add the associated - value in this row. */ + + /* Associate each of the line's fields with its header. */ for (int j = 0; j < header.length; j++) { map.put(header[j], fields[j]); } + /* Add the HashMap to the list. */ body.add(map); } } + /** + * @return the length of the body. + */ public int getLength() { return body.size(); } + /** + * @return the value associated with the given key on the given line. + */ public String get(int i, String key) { return body.get(i).get(key); } diff --git a/src/ComparisonView.java b/src/ComparisonView.java index 8e20cf3..78897e9 100644 --- a/src/ComparisonView.java +++ b/src/ComparisonView.java @@ -10,6 +10,10 @@ import java.awt.*; * References: * https://zetcode.com/java/listdirectory/ * https://stackoverflow.com/questions/4871051/how-to-get-the-current-working-directory-in-java + * https://stackoverflow.com/questions/2501861/how-can-i-remove-a-jpanel-from-a-jframe + * https://stackoverflow.com/questions/5652344/how-can-i-use-a-custom-font-in-java + * https://www.javaprogramto.com/2019/03/java-uimanager.html + * * * Date: * 2024-05-08 @@ -18,25 +22,29 @@ import java.awt.*; * Provide a view for comparing font families. */ +// ComparisonView is a JFrame public class ComparisonView extends JFrame { - private final int WINDOW_MIN_WIDTH = 960; - private final int WINDOW_MIN_HEIGHT = 540; - private ArrayList fonts; + private final int WINDOW_MIN_WIDTH = 960; // A ComparisonView has a minimum height + private final int WINDOW_MIN_HEIGHT = 540; // A ComparisonView has a minimum width + private ArrayList fonts; // A ComparisonView has a list of fonts + private Font textFont; // A ComparisonView has a preferred text font + private final int TEXT_SIZE = 20; // A ComparisonView has a constant text size. /** * Walk the file tree. */ static void walk(File dir, ArrayList list) { - File[] dirContents = dir.listFiles(); + File[] dirContents = dir.listFiles(); // List the files in the directory. + // For each of the items in the directory: for (int i = 0; i < dirContents.length; i++) { - if (dirContents[i].isFile()) { - if (dirContents[i] != null) { - list.add(dirContents[i]); - } + // If it's a file, and its name ends with '.pb', add it to the list. + if (dirContents[i].isFile() && dirContents[i].getName().endsWith(".pb")) { + list.add(dirContents[i]); + // If it's a directory, recurse through it. } else if (dirContents[i].isDirectory()) { walk(dirContents[i], list); } - /* Otherwise, an irregular file. Ignore it. */ + // Otherwise, it's an irregular file. Ignore it. } } @@ -44,66 +52,89 @@ public class ComparisonView extends JFrame { * Initialize GUI and comparison. */ public ComparisonView() { + /* Set text font. Register the included ttf file as a font + in the current environment. This is akin to adding it to the map + of font family names and file names. */ + try { + GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + environment.registerFont(Font.createFont(Font.TRUETYPE_FONT, + new File("./DejaVuSansCondensed.ttf"))); + /* If either IOException or FontFormatException are thrown, error out. */ + } catch (IOException|FontFormatException e) { + System.out.println(e.getMessage()); + System.exit(-1); + } + + /* Set default text font to included DejaVu Sans Condensed. */ + textFont = new Font("DejaVu Sans Condensed", Font.PLAIN, TEXT_SIZE); + getContentPane().setFont(textFont); + UIManager.put("Label.font", textFont); + UIManager.put("Panel.font", textFont); + + setTitle("Google Fonts Style vs. Popularity"); // Window title - setMinimumSize(new Dimension(WINDOW_MIN_WIDTH, - WINDOW_MIN_HEIGHT)); // Minimum window size + setMinimumSize(new Dimension(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)); // Minimum window size /* On window close, kill the program. */ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); pack(); // Pack the GUI setVisible(true); // Make the window visible - /* In $(find . | grep METADATA.pb), each - family is described in JSON. Human name is provided on first line. - For each METADATA.pb, instantiate a FontFamily object, keep - in an ArrayList. */ - //System.out.println(System.getProperty("user.dir")); - ArrayList fileList = new ArrayList(); + /* In metadata/*.pb, each font is described. + Add each of the files in that directory to a list. */ + ArrayList metadataList = new ArrayList(); try { - walk(new File("."), fileList); + walk(new File("."), metadataList); } catch (Exception e) { System.out.println(e.getMessage()); // Print error message - System.exit(1); // Exit with error - } - - ArrayList metadataList = new ArrayList(); - /* Enhanced for loop. */ - for (File file : fileList) { - if (file.getPath().endsWith(".pb")) { - metadataList.add(file); - } + System.exit(-1); // Exit with error } + /* Open families.csv and popularity.json. If either are missing, + the program cannot run. */ File styleFile = new File("families.csv"); if (!styleFile.exists()) { + System.out.println("families.csv doesn't exist."); System.exit(-1); } - File popularityFile = new File("popularity.json"); if (!popularityFile.exists()) { System.exit(-1); } + /* Panel to show while waiting for FontFamily data structures to populate. */ + JPanel waiting = new JPanel(); + JLabel waitingLabel = new JLabel("Memory structures take 10-15 seconds to set up. My apologies."); + waiting.add(waitingLabel); + this.add(waiting, BorderLayout.CENTER); + setVisible(true); + /* Construct Readers from each of the metadata files. */ CSVReader style = new CSVReader(styleFile); JSONReader popularity = new JSONReader(popularityFile); - /* Create FontFamily objects from each METADATA.pb file. */ + // Create FontFamily objects from each METADATA.pb file. fonts = new ArrayList(); + /* For each metadata file, parse a FontFamily. */ for (File file : metadataList) { JSONReader metadata = new JSONReader(file); - metadata.setPb(); + metadata.setMetadata(); fonts.add(new FontFamily(metadata, popularity, style)); } - - for (FontFamily font : fonts) { - System.out.println(font.getFamilyName()); - for (String subset : font.getSubsets()) { - System.out.println(subset); - } - } + + waiting.remove(waitingLabel); // Remove waiting message. + JPanel author = new JPanel(); + JLabel authorLabel = new JLabel("Written by Kian Agheli"); + author.add(authorLabel); + add(author, BorderLayout.SOUTH); // Add author panel. + + LineGraph graph = new LineGraph(fonts); // Draw a line graph of the font views metadata. + add(graph, BorderLayout.NORTH); // Add the line graph. + + pack(); // Pack the window. + setVisible(true); // Re-draw the window. } public static void main(String[] argv) { - new ComparisonView(); + new ComparisonView(); // Call the constructor. } } diff --git a/src/FontFamily.java b/src/FontFamily.java index cff4f87..59a458d 100644 --- a/src/FontFamily.java +++ b/src/FontFamily.java @@ -28,9 +28,10 @@ public class FontFamily { private String designer; /* total views, 7day views, 30day views, 90day views, year views. Taken from popularity.json. */ + final String[] popularityMetrics = {"7day", "30day", "90day", "year"}; private HashMap views; - private String license; - private String category; + private String license; // Distribution license. + private String category; // Vague category. /* popularity.json and families.csv are shared among all families. Have their associated objects passed from outside. */ @@ -38,16 +39,21 @@ public class FontFamily { * Constructor. */ public FontFamily(JSONReader metadata, JSONReader popularity, CSVReader styles) { - this.metadata = metadata; - parseMetadata(metadata); - parsePopularity(popularity); - parseStyles(styles); + this.metadata = metadata; // Set the instance's metadata to passed value. */ + parseMetadata(metadata); // Parse the metadata. + + views = new HashMap(); // Instantiate views hash map. + parsePopularity(popularity); // Parse the popularity data + + this.styles = new HashMap(); // Instantiate styles hash map + parseStyles(styles); // Parse the styles data } /** * Parse metadata. */ private void parseMetadata(JSONReader metadata) { + /* Parse the given fields. See their values described near the top of the class definition. */ familyName = metadata.get("name"); dateAdded = metadata.get("date_added"); subsets = metadata.getAll("subsets"); @@ -60,18 +66,91 @@ public class FontFamily { * Parse popularity. */ private void parsePopularity(JSONReader popularity) { + String popularityJson = popularity.getFamily(familyName); // Block of JSON for this family + if (popularityJson == null) { + /* Not all families have popularity metadata, just most. */ + return; + } + + /* For each timeframe provided in popularity.json, parse as long and add to hash map. */ + for (String timeframe : popularityMetrics) { + String timeframeKey = "\"" + timeframe + "\":"; // JSON key + int timeframeIndex = popularityJson.indexOf(timeframeKey); // Index of JSON key + + String viewsKey = "\"views\": "; // JSON key for views following timeframe key + int viewsIndex = popularityJson.indexOf(viewsKey, timeframeIndex); + + int start = viewsIndex + viewsKey.length(); // Start of data after key + int end = popularityJson.indexOf(",", start); // End of data before next comma + + long value = 0; + /* Parse views as long. */ + try { + value = Long.parseLong(popularityJson.substring(start, end)); + /* If fail to parse as long, something is wrong with popularity.json. + 2^63 is a big number to overflow. */ + } catch (NumberFormatException e) { + System.out.println("Failed to parse " + e.getMessage()); + System.exit(-1); + } + + views.put(timeframe, value); // Add the parsed long to the hash map + } + + /* Parse total views for given family. */ + try { + views.put("total", Long.parseLong(JSONReader.get(popularityJson, "totalViews"))); + /* If fail to parse as long, something is wrong with popularity.json. + 2^63 is a big number to overflow. */ + } catch (NumberFormatException e) { + System.out.println("Failed to parse " + e.getMessage()); + System.exit(-1); + } + } /** * Parse styles. */ - private void parseStyles(CSVReader styles) { + private void parseStyles(CSVReader styleCsv) { + int len = styleCsv.getLength(); // Lines of body in CSV + + /* Styles CSV has three header fields: Family, Group/Tag, and Weight. */ + for (int i = 0; i < len; i++) { + /* If the current row contains data for the given family, add that data. */ + if (familyName.equals(styleCsv.get(i, "Family"))) { + try { + /* Add the Group/Tag and the associated weight. */ + styles.put(styleCsv.get(i, "Group/Tag"), + Integer.parseInt(styleCsv.get(i, "Weight"))); + /* If unable to parse as integer, something is wrong with families.csv. */ + } catch (NumberFormatException e) { + System.out.println("Unable to parse " + e.getMessage()); + System.exit(-1); + } + } + + } } + /** + * @return family name + */ public String getFamilyName() { return familyName; } + + /** + * @return unicode subsets. + */ public String[] getSubsets() { return subsets; } + + /** + * @return views hash map + */ + public HashMap getViews() { + return views; + } } diff --git a/src/JSONReader.java b/src/JSONReader.java index 45f6bd5..44831c9 100644 --- a/src/JSONReader.java +++ b/src/JSONReader.java @@ -1,5 +1,6 @@ import java.util.HashMap; import java.util.ArrayList; + import java.io.*; /* @@ -9,6 +10,8 @@ import java.io.*; * References: * https://googlefonts.github.io/gf-guide/metadata.html * https://stackoverflow.com/questions/3880274/how-to-convert-the-object-to-string-in-java + * https://stackoverflow.com/questions/8938498/get-the-index-of-a-pattern-in-a-string-using-regex + * https://howtodoinjava.com/java/regex/start-end-of-string/ * * Date: * 2024-05-08 @@ -21,15 +24,23 @@ public class JSONReader extends Reader { /* metadata.pb is not exactly JSON, but so close to it that it's practically a subset of JSON with fewer quotation marks. If set, this is a pb file. */ - private boolean pb; + private boolean isMetadata; public JSONReader(File file) { - super(file); - pb = false; + super(file); // Call constructor. + isMetadata = false; // By default, not metadata. } private String composeKey(String key) { - if (!pb) { + /* Call static version. This has the benefit of reducing code size. + Rather than write two separate static and instantiated versions, the + instantiated version may call the static version. */ + return composeKey(key, this.isMetadata); + } + + private static String composeKey(String key, boolean isMetadata) { + /* JSON usually has quotes surrounding keys. Google's metadata doesn't. */ + if (!isMetadata) { key = "\"" + key + "\""; } key += ": "; @@ -42,20 +53,32 @@ public class JSONReader extends Reader { * match. */ public String get(String key) { - key = composeKey(key); - - int start = this.getContents().indexOf(key); + /* Call static version. */ + return JSONReader.get(this.getContents(), key, isMetadata); + } + + public static String get(String json, String key) { + /* No isMetadata boolean passed, presumably irrelevant. */ + return JSONReader.get(json, key, false); + } + + /** + * Get a value given a key and a block of JSON to search. + */ + public static String get(String json, String key, boolean isMetadata) { + key = composeKey(key, isMetadata); // Compose a key + int start = json.indexOf(key); // Index of key if (start == -1) { /* No match. */ return null; } - start += key.length(); + start += key.length(); // Start of data past key /* This JSON is pretty-printed. */ - int nl = this.getContents().indexOf("\n", start); + int nl = json.indexOf("\n", start); /* Extract substring. Remove any quotation marks and commas. */ - String substring = this.getContents().substring(start, nl); + String substring = json.substring(start, nl); return substring.replaceAll("[\",]", ""); } @@ -63,12 +86,13 @@ public class JSONReader extends Reader { * Search for and return all matches. */ public String[] getAll(String key) { - key = composeKey(key); + key = composeKey(key); // Compose key - ArrayList list = new ArrayList(); - int lastMatch = 0; + ArrayList list = new ArrayList(); // ArrayList to compose output array + int lastMatch = 0; // Previous match's index. Necessary for iteration through file contents. + /* While matches exist, continue. */ while ((lastMatch = this.getContents().indexOf(key, lastMatch)) != -1) { - lastMatch += key.length(); + lastMatch += key.length(); // Data starts after key. int start = lastMatch; /* This JSON is pretty-printed. */ int nl = this.getContents().indexOf("\n", start); @@ -90,16 +114,48 @@ public class JSONReader extends Reader { } /** - * Search for and return a table of fields. + * @return a block of JSON associated with a font family. + * Heavily reliant on the format of popularity.json. Not + * suitable for generic JSON parsing. */ - public String getTable(HashMap name) { - return null; + public String getFamily(String family) { + /* Taking advantage of the specific structure + of popularity.json. Find the index of the family + name declaration. Then, all items until the next line + starting with ' }' are included. */ + String contents = this.getContents(); + int start = contents.indexOf(" \"family\": \"" + family + "\","); + if (start == -1) { + /* No match. */ + return null; + } + + /* Restring input starting from family name declaration. */ + contents = contents.substring(start); + + String[] lines = contents.split("\n"); + int i = 0; + /* In this format, an end block is guaranteed. + Iterate through lines until the desired level of end + block is found. */ + while (!lines[i].startsWith(" }")) { + i++; + } + + /* For each of the lines in the block preceding the end block, + add to output. */ + String result = ""; + for (int j = 0; j < i; j++) { + result += lines[j] + "\n"; + } + + return result; } /** - * Specify that this is the special PB format. + * Specify that this is the special pb metadata format. Almost JSON. * See https://googlefonts.github.io/gf-guide/metadata.html. */ - public void setPb() { - this.pb = true; + public void setMetadata() { + this.isMetadata = true; } } diff --git a/src/LineGraph.java b/src/LineGraph.java index 129bb1c..3097be4 100644 --- a/src/LineGraph.java +++ b/src/LineGraph.java @@ -1,10 +1,24 @@ import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Collections; +import java.util.Comparator; +import java.util.Calendar; +import java.util.Date; /* * @author * Kian Agheli * * References: + * https://stackoverflow.com/questions/8693342/drawing-a-simple-line-graph-in-java + * https://www.geeksforgeeks.org/sorting-a-hashmap-according-to-values/ + * https://stackoverflow.com/questions/16252269/how-to-sort-a-list-arraylist + * https://stackoverflow.com/questions/2839508/java2d-increase-the-line-width + * https://stackoverflow.com/questions/4285464/java2d-graphics-anti-aliased + * https://stackoverflow.com/questions/7182996/java-get-month-integer-from-date + * https://stackoverflow.com/questions/5799140/java-get-month-string-from-integer * * Date: * 2024-05-08 @@ -13,20 +27,193 @@ import javax.swing.*; * Draw a line graph. */ -public class LineGraph { - private int[][] coordinates; // A line graph has-a set of coordinates +// A LineGraph is-a JPanel +public class LineGraph extends JPanel { + private ArrayList fonts; // A line graph has-a set of fonts + private final int WIDTH = 640; // A line graph has a preferred width + private final int HEIGHT = 480; // A line graph has a preferred height + private final int LINES = 0x10; // A line graph has a maximum lines graphed at a time + private final int LINE_WIDTH = 3; // A line graph has lines with a given pixel width + private final int TICK_HEIGHT = 10; // A line graph has ticks with a given pixel height + /* A line graph has an array of abbreviated month names. */ + private final String[] ABBREVIATED_MONTH = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", + "Aug", "Sep", "Oct", "Nov", "Dec"}; + /* A line graph has an array of unique color values to use for each of the lines. */ + private final int[] COLORS = {0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, + 0xff8040, 0xff4080, 0x80ff40, 0x40ff80, 0x4080ff, 0x8040ff, 0xff8080, 0x80ff80, 0x8080ff, + 0xff4040, 0x40ff40, 0x4040ff}; /** * Constructor. */ - public LineGraph() { - coordinates = null; + public LineGraph(ArrayList fonts) { + this.fonts = fonts; // Set instance's list of fonts to the list passed. + this.setPreferredSize(new Dimension(WIDTH, HEIGHT)); // Set the preferred size of the panel. } - /** * Set a point at a pair of coordinates. */ void setCoordinates(int y, int x) { } + + /* Automatically called at construction time. */ + @Override + public void paintComponent(Graphics g) { + /* Cast to Graphics2D. Permits use of Graphics2D methods. */ + Graphics2D graphics = (Graphics2D) g; + /* White background. */ + graphics.setBackground(Color.WHITE); + /* Fill entire space allocated. No margins or padding. */ + graphics.clearRect(0, 0, getWidth(), getHeight()); + /* Enable anti-aliasing. */ + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + /* Set line width. */ + graphics.setStroke(new BasicStroke(LINE_WIDTH)); + /* Draw in black. */ + graphics.setColor(Color.BLACK); + /* Store font metrics so as to calculate width and height of each tick's text. */ + FontMetrics metrics = graphics.getFontMetrics(); + /* Store enough space on the left for 12 digits. Assuming tabular figures. */ + int xOffset = metrics.stringWidth("012345678901"); + /* Keep enough space at bottom for text with descenders and ticks. */ + int yMax = getHeight() - (metrics.getHeight() << 1) - TICK_HEIGHT; + /* Keep enough space at right for the right half of the text "MMM". */ + int xMax = getWidth() - (metrics.stringWidth("MMM") >> 1) - TICK_HEIGHT - xOffset; + /* Draw x axis. */ + graphics.drawLine(TICK_HEIGHT + xOffset, yMax, + getWidth(), yMax); + + /* Store calendar, so as to get month. */ + Date date = new Date(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); // Set calendar to current date + + /* For each month in the year, draw a tick on the x axis. */ + int x = xMax; + int xTicks = 12; + double xSpace = (float) x / xTicks; + + for (int i = 0; i <= xTicks; i++) { + graphics.drawLine(x + TICK_HEIGHT + xOffset, yMax, + x + TICK_HEIGHT + xOffset, getHeight() - metrics.getHeight()); + String month = ABBREVIATED_MONTH[calendar.get(Calendar.MONTH)]; + /* Half the width for centering offset. */ + int textOffset = metrics.stringWidth(month) >> 1; + graphics.drawString(month, x + TICK_HEIGHT + xOffset - textOffset, + getHeight() - metrics.getHeight() / 5 * 2); + x = (int) (x - xSpace); // Decrement x offset. + calendar.roll(Calendar.MONTH, false); // Roll back by one month + } + + /* Draw y axis. Subtract one third of stroke width to account for width of first tick. */ + graphics.drawLine(TICK_HEIGHT + xOffset - LINE_WIDTH / 3, 0, TICK_HEIGHT + xOffset - + LINE_WIDTH / 3, yMax); + + sortFonts("30day"); // Sort the list of fonts by their 30day metrics. + + int y = 0 + metrics.getHeight(); + /* Cast to double within parentheses for floating-point + division, cast back to int to remove fractional part. + 5/4 spacing for legibility. */ + int yTicks = (int) (yMax / ((double) metrics.getHeight() * 5 / 4)); + /* Sorted values by monthly views in descending order. The greatest value is at the top. */ + long maxViews = fonts.get(0).getViews().get("30day"); + float ySpace = (float) yMax / yTicks; + for (int i = yTicks; i >= 0; i--) { + String text = String.format("%d", maxViews * i / yTicks); + /* Difference between maximum string width and actual string width is offset. */ + int textOff = xOffset - metrics.stringWidth(text); + graphics.drawString(text, textOff, y); + y += ySpace; + } + + /* Draw lines. */ + for (int i = 0; i < LINES; i++) { + FontFamily font = fonts.get(i); // Keep the current font handy. + HashMap views = font.getViews(); // Keep the views hashmap handy. + + /* Map of coordinates to draw. */ + HashMap coordinates = new HashMap(); + + /* Use the given metrics to guesstimate the values of each month. + The last month's views are known. */ + coordinates.put(12, views.get("30day")); + + /* Using 90day views, we may subtract 30day to get a closer approximation + of 60day views. */ + coordinates.put(11, (views.get("90day") - views.get("30day")) / 2); + + /* Account for changes in views by deriving views 3 months ago from 90day + views / 3. */ + coordinates.put(10, (views.get("90day") / 3)); + + + /* Guesstimate the rest of the months by yearly views / 12. */ + for (int j = 1; j <= 9; j++) { + coordinates.put(j, (views.get("year") / 12)); + } + + graphics.setColor(new Color(COLORS[i])); // Set the drawing color. + + /* Previous x and y values. Necessary for drawing lines between points. */ + int xPrevious = 0; + int yPrevious = 0; + for (int xCoordinate = 1; xCoordinate <= 12; xCoordinate++) { + /* x is ratio of month value / 12. + Because y starts at the top, invert with subtraction from maximum pixel value. + Ratio of given views over maximum views, multiplied by maximum pixel value. + Circle with radius of LINE_WIDTH * 2. */ + int xCurrent = xOffset + TICK_HEIGHT + xMax * xCoordinate / 12; + int yCurrent = yMax - (int) ((float) coordinates.get(xCoordinate) / + maxViews * yMax); + graphics.fillOval(xCurrent, yCurrent, LINE_WIDTH << 1, LINE_WIDTH << 1); + + /* Line to previous point, if set. */ + if (xPrevious == 0) { + /* If no previous point, use current y value. */ + graphics.drawLine(xOffset + TICK_HEIGHT + xMax * (xCoordinate - 1) / 12, + yCurrent, xCurrent, yCurrent); + } else { + /* Line from previous x and y to current x and y. */ + graphics.drawLine(xPrevious, yPrevious, xCurrent, yCurrent); + } + + /* Set previous values for next iteration. */ + yPrevious = yCurrent; + xPrevious = xCurrent; + } + } + } + + /* Sort by views, in descending order. */ + private void sortFonts(String metric) { + Collections.sort(fonts, new Comparator() { + /* If a is lesser, return 1. If b is lesser, return -1. + Otherwise, they are equal, return 0. */ + public int compare (FontFamily a, FontFamily b) { + /* If equal, return 0. */ + if (a.getViews().get(metric) == b.getViews().get(metric)) { + return 0; + } + + /* If either is null, it is lesser. */ + if (a.getViews().get(metric) == null) { + return 1; + } + if (b.getViews().get(metric) == null) { + return -1; + } + + /* Neither are null, compare. */ + if (a.getViews().get(metric) > b.getViews().get(metric)) { + return -1; + } + + /* aViews must be lesser. */ + return 1; + } + }); + } } diff --git a/src/Reader.java b/src/Reader.java index cdad0cc..05dd03e 100644 --- a/src/Reader.java +++ b/src/Reader.java @@ -33,7 +33,7 @@ class Reader { /* On exception, exit. */ } catch (Exception e) { System.out.println(e.getMessage()); - System.exit(1); + System.exit(-1); } } diff --git a/week5.png b/week5.png new file mode 100644 index 0000000..70c779e Binary files /dev/null and b/week5.png differ diff --git a/week6.png b/week6.png new file mode 100644 index 0000000..b730c9d Binary files /dev/null and b/week6.png differ -- cgit v1.2.3