From f5aa49f5e29f3a84d5a028f1d9b8e172cd8831da Mon Sep 17 00:00:00 2001 From: Neil Tomar Date: Sun, 6 Sep 2026 17:39:29 +0530 Subject: [PATCH] redesign the help statistics plot --- .../help/commands/HelpAccountSubcommand.java | 2 +- .../commands/HelpStatisticsSubcommand.java | 35 ++- .../net/discordjug/javabot/util/Plotter.java | 287 ++++++++++++++---- .../discordjug/javabot/util/PlotterTest.java | 90 ++++++ src/test/resources/PlotterTest.png | Bin 0 -> 87232 bytes 5 files changed, 334 insertions(+), 80 deletions(-) create mode 100644 src/test/java/net/discordjug/javabot/util/PlotterTest.java create mode 100644 src/test/resources/PlotterTest.png diff --git a/src/main/java/net/discordjug/javabot/systems/help/commands/HelpAccountSubcommand.java b/src/main/java/net/discordjug/javabot/systems/help/commands/HelpAccountSubcommand.java index 18042daec..6083f4e2f 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/commands/HelpAccountSubcommand.java +++ b/src/main/java/net/discordjug/javabot/systems/help/commands/HelpAccountSubcommand.java @@ -125,7 +125,7 @@ private FileUpload generatePlot(User user) { plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), new Plotter.Bar(value))); } - BufferedImage plt = new Plotter(plotData, "gained help XP per month").plot(); + BufferedImage plt = new Plotter(plotData, "General Helper XP Gains","XP earned each month from general helper activity").plot(); try(ByteArrayOutputStream os = new ByteArrayOutputStream()){ ImageIO.write(plt, "png", os); return FileUpload.fromData(os.toByteArray(), "image.png"); diff --git a/src/main/java/net/discordjug/javabot/systems/help/commands/HelpStatisticsSubcommand.java b/src/main/java/net/discordjug/javabot/systems/help/commands/HelpStatisticsSubcommand.java index 50886eb09..1eb1a0dee 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/commands/HelpStatisticsSubcommand.java +++ b/src/main/java/net/discordjug/javabot/systems/help/commands/HelpStatisticsSubcommand.java @@ -5,14 +5,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.PriorityQueue; import javax.imageio.ImageIO; @@ -24,6 +18,8 @@ import net.discordjug.javabot.util.Plotter; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.utils.FileUpload; import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand; @@ -34,21 +30,30 @@ public class HelpStatisticsSubcommand extends SlashCommand.Subcommand { private static final List> COLORS = List.of( - new Pair<>("Red", Color.RED), new Pair<>("Blue", Color.BLUE), new Pair<>("Yellow", Color.YELLOW), - new Pair<>("Green", Color.GREEN), new Pair<>("Cyan", Color.CYAN), new Pair<>("Magenta", Color.MAGENTA), - new Pair<>("Orange", Color.ORANGE), new Pair<>("Pink", Color.PINK), new Pair<>("Light gray", Color.LIGHT_GRAY) + new Pair<>("Red", Color.decode("#D62728")), new Pair<>("Blue", Color.decode("#1F77B4")), new Pair<>("Yellow", Color.decode("#BCBD22")), + new Pair<>("Green", Color.decode("#2CA02C")), new Pair<>("Cyan", Color.decode("#17BECF")), new Pair<>("Magenta", Color.decode("#9467BD")), + new Pair<>("Orange", Color.decode("#FF7F0E")), new Pair<>("Pink", Color.decode("#E377C2")), new Pair<>("Light gray", Color.decode("#7F7F7F")) ); private final HelpTransactionRepository transactionRepository; - + + /** + * Creates the help statistics subcommand. + * + * @param transactionRepository repository for help transactions + */ public HelpStatisticsSubcommand(HelpTransactionRepository transactionRepository) { this.transactionRepository = transactionRepository; - setCommandData(new SubcommandData("stats", "Shows an general plot about help activity in this server")); + setCommandData(new SubcommandData("stats", "Shows an general plot about help activity in this server") + .addOption(OptionType.BOOLEAN, "darkmode", "generate a plot in dark mode.", false) + ); + } @Override public void execute(SlashCommandInteractionEvent event) { - + boolean darkMode = event.getOption("darkmode", false, OptionMapping::getAsBoolean); + event.deferReply().queue(); List> transactionWeights = transactionRepository.getTotalTransactionWeightByMonthAndUsers(LocalDate.now().withDayOfMonth(1).minusYears(1).atStartOfDay()); @@ -75,8 +80,8 @@ public void execute(SlashCommandInteractionEvent event) { } plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), new Plotter.Bar(entriesForThisMonth))); } - - BufferedImage plot = new Plotter(plotData, "General helper statistics").plot(); + + BufferedImage plot = new Plotter(plotData, "Help Statistics","Monthly assistance provided to community members",darkMode).plot(); try(ByteArrayOutputStream os = new ByteArrayOutputStream()){ ImageIO.write(plot, "png", os); FileUpload upload = FileUpload.fromData(os.toByteArray(), "image.png"); diff --git a/src/main/java/net/discordjug/javabot/util/Plotter.java b/src/main/java/net/discordjug/javabot/util/Plotter.java index 59ed2279e..03b2321b1 100644 --- a/src/main/java/net/discordjug/javabot/util/Plotter.java +++ b/src/main/java/net/discordjug/javabot/util/Plotter.java @@ -1,7 +1,6 @@ package net.discordjug.javabot.util; -import java.awt.Color; -import java.awt.Graphics2D; +import java.awt.*; import java.awt.image.BufferedImage; import java.util.List; @@ -9,92 +8,252 @@ * Creates diagrams. */ public class Plotter { - + private static final int WIDTH = 3000; + private static final int HEIGHT = 1500; + + private static final int GRID_LINES = 6; + private static final int PILL_MARGIN = 34; + private static final int PILL_HEIGHT = 42; + private static final int PILL_BOTTOM_MARGIN = 65; + private static final int LEFT_HEADING_MARGIN = (int) (WIDTH * 0.02); // 2% + private static final int LEFT_GRAPH_MARGIN = (int) (WIDTH * 0.05); // 5% + private static final int RIGHT_GRAPH_MARGIN = (int) (WIDTH * 0.02); // 2% + private static final int TOP_HEADING_MARGIN = (int) (HEIGHT * 0.06); // 6% + private static final int TOP_SUBHEADING_MARGIN = (int) (HEIGHT * 0.09); // 9% + private static final int TOP_GRAPH_MARGIN = (int) (HEIGHT * 0.15); // 15% + private static final int BOTTOM_GRAPH_MARGIN = (int) (HEIGHT * 0.09); // 9% + private static final int TITLE_SIZE = (int) (HEIGHT * 0.038); // 3.87% + private static final int SUBTITLE_SIZE = (int) (HEIGHT * 0.018); // 1.87% + private static final int AXIS_SIZE = (int) (HEIGHT * 0.016); // 1.6% + private static final int LABEL_SIZE = (int) (HEIGHT * 0.016); // 1.67% + private static final int VALUE_SIZE = (int) (HEIGHT * 0.016); // 1.6% + private static final int GRAPH_WIDTH = WIDTH - LEFT_GRAPH_MARGIN - RIGHT_GRAPH_MARGIN; + private static final int GRAPH_HEIGHT = HEIGHT - TOP_GRAPH_MARGIN - BOTTOM_GRAPH_MARGIN; + + private Color BOARDER_COLOR = Color.BLACK; + private Color BACKGROUND = Color.decode("#EAEDF5"); + private Color GRID = Color.decode("#B2B2B2"); + private Color GRID_STRONG = Color.decode("#606061"); + private Color TEXT = Color.decode("#2D2D2D"); + private Color TEXT_MUTED = Color.decode("#42474D"); + private Color TEXT_DIM = Color.decode("#59616D"); + private Color PILL = Color.decode("#D9D9D9"); + private String title; + private String subtitle; private final List> entries; - private int width=3000; - private int height=1500; - + /** * Creates the plotter. * @param entries a list of all data points to plot, each represented as a {@link Pair} consisting of the name and value of the data point * @param title the title of the plot + * @param subtitle the subtitle of plot */ - public Plotter(List> entries, String title) { + public Plotter(List> entries, String title, String subtitle) { this.entries = entries; this.title = title; + this.subtitle = subtitle; } - + + /** + * Creates the plotter. + * @param entries a list of all data points to plot, each represented as a {@link Pair} consisting of the name and value of the data point + * @param title the title of the plot + * @param subtitle the subtitle of plot + * @param darkMode the dark mode for plot + */ + public Plotter(List> entries, String title, String subtitle,boolean darkMode) { + this.entries = entries; + this.title = title; + this.subtitle = subtitle; + if (darkMode){ + BACKGROUND = Color.decode("#111318"); + GRID = Color.decode("#252A32"); + GRID_STRONG = Color.decode("#303640"); + TEXT = Color.decode("#F5F7FA"); + PILL = Color.decode("#171C23"); + } + } + /** * Create a diagram from the data supplied to the constructor. * @return the diagram as a {@link BufferedImage} */ public BufferedImage plot() { - BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D g2d = img.createGraphics(); - - g2d.setFont(ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", 30).orElseThrow()); - - g2d.setBackground(Color.WHITE); - g2d.fillRect(0, 0, width, height); - g2d.setColor(Color.BLACK); - - centeredText(g2d, title, width/2, 50); - - plotEntries(g2d, 100, 100, width-200, height-200); - + BufferedImage img = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics2D = img.createGraphics(); + + setBackground(graphics2D); + + drawHeading(graphics2D,LEFT_HEADING_MARGIN,TOP_HEADING_MARGIN,TOP_SUBHEADING_MARGIN); + drawGraph(graphics2D,LEFT_GRAPH_MARGIN,TOP_GRAPH_MARGIN,GRAPH_WIDTH,GRAPH_HEIGHT); + return img; } - private void plotEntries(Graphics2D g2d, int x, int y, int width, int height) { - double maxValue = entries.stream().map(Pair::second).mapToDouble(Bar::sum).max().orElse(0); - int stepSize = 2*(int)Math.pow(10,(int)Math.log10(maxValue)-1); - if (stepSize==0) { - stepSize=1; - } - maxValue += stepSize; - - int numEntries = entries.size(); - - int currentX = x; - - g2d.drawLine(x, y, x, y+height); - - if(maxValue>0) { - for (int current = 0; current < maxValue; current += stepSize) { - g2d.drawString(String.valueOf(current), 95-g2d.getFontMetrics().stringWidth(String.valueOf(current)), this.height-(y+(height*current)/(int)maxValue)+g2d.getFontMetrics().getHeight()/3); - } + private void drawHeading(Graphics2D graphics2D,int startX, int titleY, int subtitleY){ + Font titleFont = ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", TITLE_SIZE).orElseThrow(); + Font subtitleFont = ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", SUBTITLE_SIZE).orElseThrow(); + + graphics2D.setColor(TEXT); + graphics2D.setFont(titleFont); + graphics2D.drawString(title,startX,titleY); + + graphics2D.setColor(TEXT_MUTED); + graphics2D.setFont(subtitleFont); + graphics2D.drawString(subtitle,startX,subtitleY); + + } + + private void setBackground(Graphics2D graphics2D){ + graphics2D.setColor(BACKGROUND); + graphics2D.fillRect(-1, -1, WIDTH, HEIGHT); + } + + private void drawGraph(Graphics2D graphics2D, int startX, int startY, int width,int height){ + double maxValue = entries.stream() + .map(Pair::second) + .mapToDouble(Bar::sum) + .max().orElse(0); + + if(maxValue == 0) return; + double axisMax = niceMaximum(maxValue); + + drawLines(graphics2D,startX,startY,width,height,axisMax); + drawBars(graphics2D,startX,startY,width,height,axisMax); + } + + private void drawLines(Graphics2D graphics2D,int startX ,int startY,int width ,int height, double axisMax){ + Font axisFont = ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", AXIS_SIZE).orElseThrow(); + graphics2D.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // Use a 2px stroke because a line is between pixels. + graphics2D.setFont(axisFont); + + for (int i = 0; i <= GRID_LINES; i++) { + double fraction = (double) i / GRID_LINES; + int gridY = startY + height - (int) (height * fraction); + + graphics2D.setColor(i == 0 ? GRID_STRONG : GRID); + graphics2D.drawLine(startX, gridY+1, startX + width, gridY+1); // Move 1px down so the 2px stroke aligns with the grid position. + + double value = axisMax * fraction; + + String label = formatValue(value); + graphics2D.setColor(TEXT_DIM); + graphics2D.drawString(label, startX - 25 - graphics2D.getFontMetrics().stringWidth(label), gridY + 8); } - - boolean shift=false; - for (Pair entry : entries) { - int shiftNum = shift ? g2d.getFontMetrics().getHeight() : 0; - centeredText(g2d, entry.first(), currentX+(width/(2*numEntries)), this.height-y/2+shiftNum); + } + + private void drawBars(Graphics2D graphics2D,int startX ,int startY,int width ,int height, double axisMax){ + int count = entries.size(); + int slotWidth = width / count; + int barWidth = Math.min(110, (int) (slotWidth * 0.42)); + + Font labelFont = ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", LABEL_SIZE).orElseThrow(); + Font valueFont = ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", VALUE_SIZE).orElseThrow(); + + for (int i = 0; i < count; i++) { + Pair entry = entries.get(i); Bar bar = entry.second(); - int totalBarHeight = 0; - - double barSum = 0; - - for (Pair barSquares : bar.elements()) { - int entryHeight = (int)(height*barSquares.second()/maxValue); - barSum += barSquares.second(); - g2d.setColor(barSquares.first()); - int start = this.height-y-entryHeight - totalBarHeight; - g2d.fillRect(currentX, start, width/numEntries, entryHeight); - g2d.setColor(Color.BLACK); - g2d.drawRect(currentX, start, width/numEntries, entryHeight); - totalBarHeight += entryHeight; + double total = bar.sum(); + int centerX = startX + (slotWidth * i )+ (slotWidth / 2); + int totalHeight = (int) (height * (total / axisMax)); + int barX = centerX - barWidth / 2; + int barBottom = startY + height; + + String totalText = formatValue(total); + graphics2D.setFont(valueFont); + int textWidth = graphics2D.getFontMetrics().stringWidth(totalText); + int pillWidth = textWidth + PILL_MARGIN; + int pillY = barBottom - totalHeight - PILL_BOTTOM_MARGIN; + + graphics2D.setColor(PILL); + graphics2D.fillRoundRect(centerX - pillWidth / 2, pillY, pillWidth, PILL_HEIGHT, 18, 18); + graphics2D.setColor(TEXT); + graphics2D.drawString(totalText, centerX - textWidth / 2, pillY + 29); + + int currentY = barBottom; + for (Pair element : bar.elements()) { + double value = element.second(); + int segmentHeight = (int) (height * (value / axisMax)); + if (segmentHeight <= 0) continue; + + currentY -=segmentHeight; + + graphics2D.setColor(element.first()); + graphics2D.fillRect(barX, currentY, barWidth, segmentHeight); + + graphics2D.setColor(BOARDER_COLOR); + graphics2D.drawRect(barX-1, currentY-1, barWidth+1, segmentHeight+1); } - centeredText(g2d, String.valueOf(Math.round(barSum*100)/100.0), currentX+(width/(2*numEntries)), this.height-y-totalBarHeight-10); - shift=!shift; - currentX += width/numEntries; + + graphics2D.setFont(labelFont); + graphics2D.setColor(TEXT_MUTED); + + String label = formatMonth(entry.first()); + int labelWidth = graphics2D.getFontMetrics().stringWidth(label); + graphics2D.drawString(label, centerX - labelWidth / 2, startY + height + 65); } } - - private void centeredText(Graphics2D g2d, String text, int x, int y) { - g2d.drawString(text, x-g2d.getFontMetrics().stringWidth(text)/2, y); + + private String formatValue(double value) { + if (value >= 1_000_000) { + return String.format("%.1fM", value / 1_000_000); + } + + if (value >= 1_000) { + return String.format("%.1fK", value / 1_000); + } + + if (value % 1 == 0) { + return String.format("%.0f", value); + } + + return String.format("%.2f", value); + } + + private String formatMonth(String month) { + String[] parts = month.split(" "); + + String monthName = parts[0]; + String year = parts[1]; + + String shortMonth = switch (monthName) { + case "JANUARY" -> "Jan"; + case "FEBRUARY" -> "Feb"; + case "MARCH" -> "Mar"; + case "APRIL" -> "Apr"; + case "MAY" -> "May"; + case "JUNE" -> "Jun"; + case "JULY" -> "Jul"; + case "AUGUST" -> "Aug"; + case "SEPTEMBER" -> "Sep"; + case "OCTOBER" -> "Oct"; + case "NOVEMBER" -> "Nov"; + case "DECEMBER" -> "Dec"; + default -> throw new IllegalArgumentException("Invalid month: " + monthName); + }; + + return shortMonth + " " + year.substring(2); } - + + private double niceMaximum(double value) { + double magnitude = Math.pow(10, Math.floor(Math.log10(value))); + double normalized = value / magnitude; + double nice; + + if (normalized <= 1) { + nice = 1; + } else if (normalized <= 2) { + nice = 2; + } else if (normalized <= 5) { + nice = 5; + } else { + nice = 10; + } + + return nice * magnitude; + } + /** * A single bar which should be plotted. * diff --git a/src/test/java/net/discordjug/javabot/util/PlotterTest.java b/src/test/java/net/discordjug/javabot/util/PlotterTest.java new file mode 100644 index 000000000..c7d6e4784 --- /dev/null +++ b/src/test/java/net/discordjug/javabot/util/PlotterTest.java @@ -0,0 +1,90 @@ +package net.discordjug.javabot.util; + +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.time.YearMonth; +import java.time.format.TextStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class PlotterTest { + private static final Color[] PALETTE = { + new Color(0xF9C74F), new Color(0xF9506E), new Color(0x6C7280), + new Color(0x5B8CFF), new Color(0x43D9AD), new Color(0xC084FC), + new Color(0x22D3EE), new Color(0xFB923C) + }; + + @Test + public void ImageDifferenceTest(){ + List> testData = testData(); + BufferedImage img1 = new Plotter(testData, "General helper statistics","subtitle").plot(); + BufferedImage img2 = readImage("src/test/resources/PlotterTest.png"); + + assertTrue(compareImage(img1,img2)); + } + + public static BufferedImage readImage(String path){ + try { + File inputFile = new File(path); + BufferedImage image = ImageIO.read(inputFile); + if (image == null) { + throw new IOException("Image is null."); + } + return image; + } catch (IOException e) { + System.err.println("Error reading the image file: " + e.getMessage()); + return null; + } + } + + public static boolean compareImage(BufferedImage img1, BufferedImage img2) { + if (img1 == null || img2 == null) { + return false; + } + + if (img1.getWidth() != img2.getWidth() || + img1.getHeight() != img2.getHeight()) { + return false; + } + + for (int y = 0; y < img1.getHeight(); y++) { + for (int x = 0; x < img1.getWidth(); x++) { + if (img1.getRGB(x, y) != img2.getRGB(x, y)) { + return false; + } + } + } + return true; + } + + private static List> testData() { + Random random = new Random(42); + List> entries = new ArrayList<>(); + YearMonth month = YearMonth.of(2026,9); + + for (int i = 0; i < 13; i++) { + int segments = random.nextInt(3,5); + List> parts = new ArrayList<>(); + + for (int s = 0; s < segments; s++) { + double value = random.nextDouble(50,1800); + parts.add(new Pair<>(PALETTE[s % PALETTE.length], value)); + } + + String monthLabel = month.getMonth().getDisplayName(TextStyle.FULL, Locale.ENGLISH).toUpperCase(Locale.ROOT) + " " + month.getYear(); + entries.add(new Pair<>(monthLabel, new Plotter.Bar(parts))); + month = month.plusMonths(1); + } + return entries; + } +} diff --git a/src/test/resources/PlotterTest.png b/src/test/resources/PlotterTest.png new file mode 100644 index 0000000000000000000000000000000000000000..66eae255dd331afe1e1daff63ed3f981796bf500 GIT binary patch literal 87232 zcmeGFc_5Vg`#+9Pb*j^8(MqLErKp2cDk)5bw2-7iVN|3PitNKs=cG~`L`j)bDny8c zFr!nFEl0M>8pghkb%vSwKA-nJ!`Mp8`F(!x_gjCw>cu_J5g|oAMcpZ%4#IBnMAM}@~13{ zS;*caKaul)o%kyDP2&DrV$+RroPX<2ongrNC+5Qig$3MyKQ9}5d`vPesV?jA6H>$6 zJ`t239jX+2_$lYab4Q7;u(#Iapw%`z`ALt3KN&D}>mF^?CK;>wePl$dJbA0RP}4|N zWU{=*mgcvKVzj$vnws*vHm;tgz1OrMD3dR>94u%3#+n+x__2wa=k=)yK2l_{(kB5M zu`A*yc*vGXo(ew7sw!EMuGeH+zdc##hNbr=Xj?=ca zS9^ccsEghCygiS#QJZ-4MX+jqASxIQF`D7?~~Py2D?XAC3oRPg>^65CTHvg z^6y%FW|4}ULm>g(_l?8bdn(M+@Bgf9(L6&>{pTv-X)AJJ_SJ2cXcT}zp`awS*tF->N-TcyAPO(Wg6u5W}yC~xn5>RkTOCg^!F zvT>k##2&2Cc*RQyDREKw|E{Yi3)NgXwc%PJ-!%5 zRXQ$3SDKOcTr~BKLUdyjl=2o6-%QsxC*DxDJ5qCeK34X2g`!*Q18HBZyec5UxT@Px zA)0C}cn3;bI<$MQ<6XB;y|Nz*)%^;GHS*#cUE+4Ht~OT|$Q)GGNv{_%qQyV>)F%7U z9=&t8V@!eSJFvT%!>7oseuVJ3;Y9$f=wm{IAch4f#=Qxcm zquKLqy$Du>vgff}v4Q<#IcpY`|yejEE`R}X87Y&%9n_IWB;o6jwgF*tPh1NTs; z9y|(YI_)fi1npe#< zyTF;X<{ z(MY-+qZ>P-Hp&nbzeX}%of!TilzW@PLORQ+4gQn^PP1Pcoc@6QC$1fzvpfX!(9C?%iAzFsLWyx8 z{C1@H9y(cscm{6Ja#D5NCN=4s8Qr?R#W*;`iFRM?<(+09i=sI0RbeihxK!Ru3 zd|EA<2rN(aZlMT#fY0}__C9DKK-wpsv(|8agQ`BguBb2TnLf(~Nx)`FfoQu_&-Kj< z?wqZ7W8|yrf!ZH)ipGYl+)GHHsBKC(TG4QIfaO`qL^X3vHMlctd7x@=1rTEF=K7@? z8>ZHUqBqaT@AArdlzYx3k_=opZhu1PySuygqPj2J?6NLVhxBh&u5=xd(Gh;CFq6}S(Es+#--nt~>oRw0(dq@s)fKM00W!CSr6Mqf1CyhDCHxLfMg=cYuXbdT*KxJHS=_4=lolu6STq&c zI%(k-wrGMO@P@ zoQP*1)WAmjeu{;uZ&%Yo ze4>s_Nw;Oby;3pHWlhrr`4f*OtC<(XZa%$A<~b6Cw^{zH;3Jj-g$+fT%6R$aW#tE8 zYTV#bkT-Ep>QmIVGt5`!q<2hViW34DL7BCGeY`#uh_|`&esCfP38xZ*OnD4jnv;Cu zP9D0uiFD^9l8G;GehB7KQqRx$8se*2ILgq#-mGOsTK3U!2 z(0edw)eBRrF=`ad3p8O&j1m-}iHYrRjJu<=HgYA58^w2}&)k7N zRTr9`I?wFL0kPGhnmU@+(?pf!kh~wfx!l}5Ie7DZG&pIqRT>xVbpsF%gkbcFjdkSD zl`GLJKH7An*3fu6xdgLfK<+)nA1ozg zh-cOFL6!iIQMMO z!B+v~AQ-$(`n`m#kF?h+;<3QP@LOxShUJf*vH1{QwOP=KMtRr%8%~+ z2AUL-vNCO`Q92)ELuZ&s?QgsLmCZpRfw%-z!VY7ymsjZ{GB9Bryt@Owk+Lc-D7+Im z5Ask!ZX8etayIk;(jjIb>7af)zZ&?hSn_FrLNt>|Gk#E0I zn-b~7Y$2OKWkpgI#YUIq@FMDIS9>~J!ZRC*M%^-PTY`L9enq&R|k*w16$MH zduHWiwY)NrLuFARPv2Kn-{8J0dh@!2q&9fB6ZKM2b71oRbRvPaMTTfxoSYGs&WO)d zHHid{v$3Gh4D&?lZ{}qqiJtRpW^P7Gxk^4_j3AVh-qtCAE{}#Xm+SOXftz>^^yn(( zKDpFy!(E@=XBu}>?KtXj=)RXx&_)r>d-3+n9%$r+8Tm#hM4{TB0gb7dC;#(Tbiz^H zuuX{QK`nzIK1rjq=H%4K%`a4u&s@`FE!WI=Jcli&oxjRa<@q9x9S%Ix-7xX`xj2y{ zpxEfk>jsa-@WDCWhIh+T4A9t)semY$IEm%|>`qt1ZBWRb|Fqxyslyr#F>nX3(qmTt zn3qJX)QZicg`W?4j-nY@7UYRd60N?e zdq}y|BY58F@Eb6vffCC<_%5(X-%krteF{aVxuqds;<1eceK%4(KunpDFIoMtt?dZ9 z?;2`2?gqX3I`Q&jC2$F_`_6*nVeM{4$U)MrBTqkAdt~+AAS`|_`Tf050HkrsI?eF; zckbY=v==)dw8P!!1kw1td9y%tmPO*DRJ4?*M2y<}Mcy1BeVFEroDNMtIX-_TobN91 zGDF=ZX_L#k$*J=s54X>YuS+qZc~24~Miy=IhQ(u`1kTqagP{=p1XKvyX%2`UIaEf1 zU2<7vZ)AF1%@$bc);^<(ezbY)7VC2dUe$mp6&=M>KMLWPs8V@nED_#eu4` zr*vfcWZ4#A?;95*WXAdr-Coo;!3bkg%Sb)2#}pJA%QTXsQoE3fgHvVG>gHYa zYD-@o643k>k{d|96ukM^2AMb{UMNIMcuD}xFYh-(1Lmo88*_665rFUktEcOtq7)-} zR4*FxmHVnlF}+J4`;-6;gAzKiRJ;CmCR`ohD3!eGt6$2WeD!Ng76RFtqZeQf%8@YBBL$BEQ~x+l#zgV%dv11XZWIlC_({s^4oZzCMXGJN zK{x6!QM^ta&wQ$#v*7(XEn;^>MTC5*ac4*5ERKN6CiHf=th23dZKw)p%Z^gB(65_T zyD1b$_;5~(-7T-Qche0HPl~}{%bMkHqzb#YhsNUST0ToJ7TK zIaEe`i^Tk3840Z*6zO6x-LfRxl@>vGVmEju$NkOey;FA>@14s3J8HogjC@x0qO?1D zj_s~|I61;Cthc($1{cG?iNYHKCrbP4nJD>Vz+y&}sGjiiChj|d(z;TffaglsN$V0-7i7w!RgD(lPp-cjOQ^Kz@1RKpg^p#U* z3?r{nhm{sKo$BwlIm+90CbVUbBv(9PrN%pX|G4*_SUIoon~hV!b`Q(`1^8wy=aNY- zbXPt@)`IxnTTh5*Qr%}zT^A#lym3LH!W);&l|Hr#%B;GfyW*Gw{{x-YP=|Z^4<5~3 zv@pDP6}M;9y@trVml5;vw0kJ-OzQ0%pS?ZXU+z?0wQ0Q`U4!e>Ek}WIb3cJ${2y}a zv8#?n_W#jIY~$t|bS%3Kw>gsa@2QF?_q~GL2yU~=W!)Sjv5($~E?GSi+?W9w;_P(q zGcspz4&B|0xM4Ns+)AnQ37#FR#M)A`6u1wUoXJt}?S0nV@`2j^Icj0wkIm{8d|T7K z8yq?lWK&@u9Qosz{K#dU62G-wPnJdZQDN?n&8qgDsJTkjHk8jw{m5-=C-unOSn$Ho z_K;7O#TjBB&S-vGaWCWZE0#=pR3w4by&UJ@!CtX!;2&CN<@8rrgu=KOBc7E;qgQw> zODVa}vfigS@K2>~>YhtDUj^TL3O+`L^aNSkUZYy>3Ciw1-*Us&D)&9*7(7L~R|@gO zmcBDb0XDTHR5l);>Y`1GTu17aD;?g$-T!up};WYS}2bIIVZiWHFbU zVinlC`HSQ;IIIH(Q=%WP;A<8rEhdbW3$g9YKGi21$>@rHU?ph&e-l`Z#o)0R{69Jd z4>I5u<(@1Gj{Xo0CS{snXvYbsF1L~1<5kKj5=t_F+xKeMIj5$qpm3sJf(w}9>Asj7 z;K`@zcn+<`wFCxLFDLo+HlaU*Z8l7qntIkQ`OWkWG}^<)7bXdNPso#CM-xDw zbJDZ`@r3w?-Ok8uG)~{t=_ZUZ;AcvMe?|%S|Es~HcQNPC58-;vA)?FemlM(6HwNYt zOiNGQlvJVAQcjZ5OM~|y+ij1$9qy8weH+_=Ro#^>x&z9nm^2C zS88W|enrbZFx&%mHV^0CT;#J@oIwi>L$yx=nK zrYD7yHxh0@r8{)|VIXds;Hk#24VA1Fcy^rMYv=<@I?hvf86T1U*xmi$fr;*}p_@48 z#(Rqfn-m)yx21JwHH&Hvv&|Cf397bnp~CKMh0^C&wlUAj&^cKzX^#vr)KX2gop1KL zA9CCOdy0LOI7oatdxq|mx5z(2R=(F(PsnFcr_cz>3Xw^`+Y&z5vb;za_FHsE5Y~GS z)o6&VRf2hUODlcn%c$Y4=0~hjd8keYFNeLS=W*t*UXEjyA%+isHpG#x+U=obr-q|r z8JO@YP%yFPcKB3Uc$IRzrX&e^!>T%&)M^n5N2&mCAztk|#H&++tyhpddmnL3WiK=p zzJpmRtg6Zw)HwP%!+TZHja@A?p`aa5P&4nF(gOEG^?Ux-~T;H9Y_Oe{FosNbsC!&n91}0)#@7lPp$YJ}fz`!0KVsiM@BZYvrF^JrN^#@^?po*^A2AX?&VHzx;fry`ua2j?#;Dc#m&W>`CDBi zhja3zh3_6>^e0?j=lg&8D90IbxsZ1w)P$GZFsrpLwhu1KGy6qRA$#2r3l7N&dq9mfpNy@>xF?<#MX7g}i%j`nfyCu5RG3k~J_l{hEs zA?x6Eae9e+VR|y)&sn)E+xj&+-+J}I&6;|r4B|dc)>tjitimKn^WN;1eUjYFmrfK@lB{ja7n`ZaGh2qtcq=I)h%Kid+zpMTXVJtsPIRrSMU6F=^rz< z_y5l#Y@*BOvlkZ{+O?&rGT(%kR@ZA%=F`LXQ!aw19JeED_dkyOtGB4;?ljSyl^Pv_ z6dSMb$vO-8l2jHNM&7zEzXx@lmhcTVI*;2!i*n2ox@I_RU65lIXDxuq8FJF+@4Y<= znj2p+yxmX3(dw28cRhp)CT_yIf zdS_##PVqS9n|6y}`%E$BF*B=Gj0it7o3?|PN7+1j9P>a7W-Km&as9Dq4Z1u3%W_yt zOw46g+@|Y;GV`0qcSX!*F^`+i7?gge9f;pkvX&8eV#}aksAz22T_VjeGLjthi);Js zca>~rqzwNHImWaIk3pMP1?euIa9?e_pv=HbukZFrX`L`bHo|qt2PBqD-Orj3KL{G= zU;cz!C&D-|kiLgciS(P*xq*NvX=XIB@^G%6cb2+xOpr9e&F__|UPb}- zy8OI`@_{(WX{j;$E%oh_--FLlNt)!#>ta(fJ(y`J$<-s=f_6ry86PJdS*o?83RjUP zmK}IOcM#ZGqu7YG-53@VAuAWzOFz~gtS>M~b<@;)l!dMo#d!v}^(-@psgw+MLfPA`S>T3A$?kdgg`$ydT(<{&WGgQ@G zO0I@0J33NC1BO=`ZM#l`VVra*K1*|Vr69?5gpH@3d+I?giEvERNhQAV)*L~0<}$lu z(yB##!e3}LO|LIV_wKDuNe&!gXLAYTl-;iKk@(aCqWx1mJ+@nCgv*2}ozhx$&g?^C z{R-0=@wRL6#PuVX=FNchp@i20$H_Le!cEuXY@a5y?;EtJ(PO^_xHSwH$Rd9TX~N6S zjy_I3l$GXv6MSG;>r7Iw6MB>7&RPJ0yjAcPfx!;2NwGN5CcXv>O9B0-FOIGq$m}>J*Ii2P*}-mTp!U0}e!vT})}6iC_uXRWb@*qUv(S3c^QMuW z=bBv&g{JTN!lZRaHcs4yr!)eGN{U?+txMI+Jtt8&om5d3!3h}1HrMN_lKI?O<7BQ5 zIOoPU(Ls&BlLFjWi!9r#V`Unuk_pUg^ynA`o;dg+==ondZbeT#?KD}oRM(Xk_Y^J4 z4=3mlRx@PbJ(FhgC}87biRD>uI{1BDh0`N?jdW~RjQr4QA9-4pgrOF_EqR1(WoO6P zu24qYe6HFw`;j(uS2WS5E9Y)QE+6yi{T4Yasv`YgdE${^9>1sZ{mzKqtDBoFaHxL4-kgg*AWh3deYAs<3*Y*8bdkt4!T?#W?1zY)iS>OpqJlE|Ras8pW9q*9a(vlWR`gDsrwaIHD$zH zF!TTEPZUWzqW=Ve|B*Mpg8{`{%5+A(ru=Gofe~(fTHWr@?jk{}wqsZPx=Ipmh2s{E z@MJo8Qoh`EZC4mk@kPh}02RmDdn4*5`V{q0A&3{IGh(^%0&$EXQeS`?FXqw!qcvIT z%kiZD=mVOK%NERZKsbg0JJ{!~_Oy1FM>Md;g-7p=#*(Q=l-b?Ts!M;LF)rn2FlS~O?lW89yG#AoB&+DV zl@0+!EAWhy8C7Mug^2n1EDUDjzPTr^ZpZB12XM}M#$@Q6-G|;Ot}|LD+Ah z7>wY`-=vh5!XIK1#Eh2tN_ot&nFLsm`)f4jB5lH?`2S}Q_hN`W^laSRHFt(NO;Vh3 zR4U9V&QIt5>3bjjCSi%Ult+f|+bOaW_8t&aDOt@>p7cl^ksZ9CjV4!SoA#RgZktns zT{K@w{no1AV1UGw>B`JWy>sPGl2n;EM-BRO5y`^eI$3ko;S`l?f*1@WS5q8=v!@u5U8^PqK@B6~Or!9cE1M^|o-f_^j5i>h`_3%Gdo|dU2lpKwDjy&!V~ox&zMD2BCP-)cbZs7ZY)${zXeI|&iHm84_{Q`bEC~E z{2&p5@QNfBX_sP7v~gN`e;NbJt|zux0~Y`XxcTB#&!cSICXD!;Kun^cfe7LSo0KF9 zC6${F?Mf75C(tY_J!;8By;aBMS67K)1pp`mS683Z0rmru6Xy?BR)nnLt$?NvWCS&& zzvfp2KA3tb1KN&ydZ$r^H@}ZxmyatwE0(sq-4=7{^17gyYZ(t#{-Xcz?UrTA;5trA z>)aXxkq-K*gtoCgs1qO)PFEZ z2A1)i#R0&%Lud8y!E3~?1B&dzm7)Xyd*eYG$F~-aBkI_*!vnG=tWmtNjTvDq z0=!i$^&emHX&9A5F{dX(%%QcU6AM*gbsTs+P(=i8cC<4Ut3J8-id`YDJZ1tbwwHwm z*+BpD%8QJnc`87Xhdlq3(w>jG6>3^A1LB&9tpl&1Ph4)ymmVHeVXBl^tx1qulK65D zh{p4mrTy)wwZZHDOeb%H&no?Nu7OcXBhUuL?LtXt<*dfgeisNR0=}a~N3x{&;9g`E+N=y3O6uLUx zAu0SFxXBWXw~kc3_M>P`HJqZa9qcj=SNgbUwYoCVZ8O87Xc5TBWY$R_PN#{-*!O^4 z8c0`x01_B9u_t;ZTW%%5`kAgPOiS#!&q4DYdVA{I!hcSAb)#jSGPVz#C5?Z|{ukJ5 zq@cz4EGLcJ3rzDH?B%d7Xwn7WrLDit_POCoedq$zKGZDXX-U`X<6k>yz32$9r|xD+ zg(b`exIy3f1F=RzSnA_-ka|G0dCA&&gZ+C^Z|OgYH{YvLuxFd?h#kj9w2`RR8vb(& z^35bQ(@2s#0JIrm_E|`X1AhMMG4%7$`k^m0dv@kd@*yfNKEKqItPs~di)7}o4*2yD zK{@Og@%+N-_DJbzSQH~F?cRu+DZojs6up6ebzX;tV3i!QOv0&j;$p-F(R%*;tmJK@EXq}+(pg9TBapOR#)Lj z-J$@g4Lu|W_46LG-Kb@^KzgQqdNOG6EE%kDkq{J3@g}BQ-P6q5(gLoFGsDcyh{5jT zoCZttR;ZOnf_?h}fT8;%>m1THv!ih($?wg?yE-*{=3|CL)6d)rfCmZyE!OCY7uahv zbg3n*_sQ0ihB1k-PEVA$Nbk>nBJvkY-kK){JS^Sc)L76KQd)`obLRHmXXvhTIX$9o z|Gz*A*Mj|?R{_tx63lYdR)fZo_%$@jCQ?mRnvZTOBFbHKw7jADNt=E6ukmyavJPzM`Nmp`e~9;xG=gBH}&HK+*M!{XN_dVMxvcT8jCsrTho z(Gska^*qCUxX|0HP6TAl#tHf;K4Ksz8>)~O zJEaabD5ucpzP*(-q!}2iD}A5Ku$z=4#r+zt@ZZ*at~OJwORvt_!J#?RmC28p zbajyJ0{q&{-v@GlErmUMQUsh6_0aKab`M5qpwx@u$$6mM8XVXG3u4_bBv^WF``@%k z!`u+3%QbBi5}tN{?gCj^{316i4-~)uxWd=7mzcL}k1sjcTh{|1YW`L(L2b5QBUC$kiAOYfpU z^ZC;yC-@a_i8yvZbbWdW7hQ97C;EVcS2ps$&baVv&g*b8z)=R%x$u=^6U;x(qX_=P zn+vzJXCA%Xod@q5>OAPaq`%&wP_0mFrqtABDsw0d2&Xsn!D$SJd;}ozIq%V8LSJy| zu@l?=I90A$3IIXmByRn{u^Y$jn^BZ{=J>V@@ylyeM-ZOcP9qe{{h;tb0CR-o2BurY z!w!mib}J~DMiODhEN^$DZX_#I-i1dMCjdA((h)2asX1LIjP_6F+!2&RBMEbj@&S1V zGb<5Gj<)`fEbF?+!3R^eMX{t7plDb`E>fqrCw}zJ;d`nGbk5P zM-&aA=4D-D#eEQS?105R3@b`Q)wWfgZ(hkVMv8#|Oq^OUl?l&c$$>u}4)ZE#Hv zC&SVZ#412pz+=FIU-)+tS$d1t=9#~Cw3I=z5n%%QKP_Wi+fL5LZ4vwOr@JlQJHIWx zbEQE96j^J9J-2kfVDgf&%;3w|DV6Vl;W5Bhi7$>h^!dM=m@@1jf>92u}+)I##=?gJx`_dl*ve|h9h_hd?>)f0R z$WxH{kOws^k`rt$9A2FXD$mea6b5u07|WM6Lpli7j&4B&MoEK8-PZYGLi4U&k)Id3 zbb!Dbji0QC-L4&9JoV97y8~ z=DPX2hXlcpr-Svrjmxn#{8J+M#Q?~^3R$-Q1xWMSFUM)Gmh~JRlE_unp zF|c}ggP-S~Z!+MKuUm?H${Ztps*ez!uc!Bf+78lh`LK@b55QK@hwNnigFd85$dQFQ z>6!je4Wp3o{O~)6ZV0B6sJpMInYAAPUHhT7F}cF~@RK4fmbm6_f?Yu1=;Yx3n$mga zKILGXC;OUMxZc$gSszJk3#Qx8VDuOC>)O+amBnl7od;l1xp9m3{eqZ`%c zsl1rAgQJwv=Pj;ZObXL9-G2*HlDl&W5z?iyc^tCB|*MjwPfBrAbJ6yZ{Mr6Z@qD8i? zi+N@)h6(e|_gR6h)ynK^pho4uxivaZd{-;Vc*ib~R@SrivOjLBCoU;B*J0Rga_=`B z0^3kkdFC~)gYp9;AXt9PwG9N(efiW0*gB#>Dbf?)xqBU{mu>fcz6k9&5wCL4Po)4tkga$8|#I z4h4ww!&u6#hjXpqARg@PfDO8unDweyzzFy2sXFLN9i)`q_G3Q?DzPKZ48)K*xx4GG z-zRE4AI1fyX|hvnBzC{r%>f-Zj!K*Lwm;$n4HW&$9CJ)k$QAE^n1AwP_eK|Gw$mBd zIq3)WQhAT-kRK0!0ZQxPc2n=%m$X`jcP!|6a?-@0^`QdSxC}tu2um_3OabfYt@Btp zT}%y(vIyduBskrk{=?46o!ilrZG^csFyun+V{owqV6$V#naUkLE_T{Y#adz2JFRgG zcr~ZCPb1f<#tSoTfIty8XmHuKMn^8eZliybVVw2xIM%G%{A#2r%QfLC##a}DnGvU@ z1N;Etiw1-1OCc_rYRO`W;$5a01-NB=HgMEQfy;iFYPRJdZT?XnNfz+2^zstgd*#Zt zaN?(5P>sfpw~>a{=#Iqq##L7wU*w1XU44f<+6d#=cT5QV6w=ok08|k2YH+~=svJGJ z*^-^%1P6QDRVWzXG33I$-gbRik45Pca*T*@PUjo(uoNnIy#+)hq{z(?}l~Gu?)`}vSl}?;`*rusZ42SFemTgZFmJA`T)OlSovzC#mneOCBttht`omDyGNOD0vS3q{_ zLy~1y8}{g4ejfqmAKiM(RcU)1l2ArqRAvem+#J+%Z2|O+cWYJu7GcmT z0p*qmxcV#2b1abMoOzdPR-PzDpA;rwQ3B#Jjl(A{yA4>T7k2kMyaI@u^&|U@`aR4# zT5lHK(wd66AZObpwu9a|DOiX)%)af!i zva_#Q8JFvS-Er()kNQiX=KAD2h)JO=VEE;MY%!lY=gTlFPV1kTu9GFbrfs?`m&1hn z<9@%DDt$KbRvXOeW+-7jFMB}eplm5cOh0Kdlw|+pZ>p2s% zR!=9L$WQ7ZOpq!~>Z9>N5xUWTtA9o9G-bX9 zVQ05rN}TMSj-R~jcIiXl?P}6*8$FWb^JK9LJ}QnMhf%$~B~JL$>$%{&30t1ErAB>J zZJ6M^p?&MvN5{nU112%4_F~riGzO;oW&6u4U{uex=}(vYH+w?Q&cB77rOSlM<}bV- zu(UI{iXM1Db$Nu^hAibB8$GsSwt7|VZMwX1?Vo0yrR-jB%3QacK!2)O$S-Aq?*rwt zPDp2et96q9Z9D2kWTlnPnw&jhvD{3p05u2yOw;?5$Dgiob`$!bJbv+%gmGP%lz(R~ zp8gaZ z6wa@I4gAYJbyx!;r?lPk#J->jC@+BWaNfYkt^U{`Vz(sXl2A>Jv%~U#J(2{^@|~SF zJv)15&w(~skwV4s;BSI~!JH|-VAh{%s&qR!N27aoR?05NrnJ(z3`WAjemj?7{@!IFYOUyZ>mO+w z`JL+`B7Y5nB3%-(gT#>aD^>)z`xj^~!u^-cvu!3cjdQ!Vbo0VvBxC;+1HYa6;BWV9 zXO=l*oJ2D37R@p__FHKgeQs(}+Ooe9i6$*{sB1f8H3w*30Vpm>es;~yvgxstH(k0^ zbMmsoV_P9FlV#4}Z~1kRSAH3fxxL=nT>06RRfM9pS)SLuEw>_e``vnqtrlJ`C|2); zsAc@=_F(7@z|Ze_-xsBVSGYfFIhUopbK{}>aY$h9SGICc zgZPtBKGsTax%ihN+edB7?)LKAX{jA%l(1@d{F9U;LiN=2hGNN*CLFUYZSx?x0p~QL zr~UppOFc765D0qICJL{P)yqmb$cpw4ckLj&0^_>rdU}N38jT?WLDYgc(Wupo?IQe9 zdAyz;n-*SAufrF#1;?ht$Hm&vzj*SEt)pL&rmvNBzd z%xaBHAv#+8v}0S+FN#x$KGfFTc)ZQWf}wVam=BKNlcIf#jxdvThQd3JA1qc+pM$X}0{dgK%=iA@aKL1f8Gmg8IP0ZR7OFG-`5+ z9i3?VbHj(eoc|$YbxHQ0F#@uBZM5F9)OQGcg@2Z3Z;dxaeh{kr4s*Iv9km>1O>K5t2jOWS4*HI? zk1wZhO3l)x#|K=4fVmIeA-Lh&!d91!ygE%mlU2Sl;73Je;-BT+d&8TuYVSbk`W;dS zU;g&(_0nWnZF2q53!=rqk4Sq0QY7^Q`fpeLG^9T22lL-9ku_*(^dp-7?**}=`6Y#& zLVQ8kW|u%sBz~qQ-!#i&Coklbxg*51uU$>QNvLvy7?yBmMJbKeNVIKj4KVz2MRydf zEc)t|!4dmqzTDf%hvZo;yU8r7J)3qPk5&G1#X1S8zDCAMXNoTplBgB#5tXiZ@>)r9 z*Y2-W(kQ*uU&RcLaYwqAkPgwiHN^4uA5+-OZ9jP7ayc8>2-?fZ+B8E+JljT_Jk^ZL zg^d7bzG5vGJM7E&B70c?Qi_$@YqHv@=`FbRukl~1*XJX}=g9R)gd?9kOMH)fH;s0k z9*{D%J*m!L7l{Rl|^MmCJ+o+Q{9Q3)OeN^yCX|y zy<(uT?CIAGuRR>vgu)>z44!IT|!@gN+f1-sHb@FnP^2XQcH#sFgky9YQJ@!;9w2D(B zuW^lj4pGnBV^1VF6@LT;vQ11S!0k{`2K?rjLJu(Z2(H*1T#7_ov~ zUkwF4*@(bH}U;K^03kv9dHj7>inpK63Eb0li1Vr&}T zrkPEn9U~j^f0?=ZswY_T(VV@z{PC$`^nja^HqFZJ)>OgC*>w&d)3&LCSd@le;J{%42S@9Xc*&%xa#`oubTOhbCW^yX6#+Ue<{y;01PA}6q$19N zeNT*d?nbQ!sqN$(ls_8CL+K28U;<{sIY1{BRX z)L96b+(-Gwerzxzftirfwy4V`tT%a_N;XPuodx_|>2K0VO9F6Mvu&0(crKIA~(!*-wg)rM1|^ zLq(R9*;@7FXdc2O&EOE5T_El~Ig9YP2ooo5)08_I)(pUUWIrjQ%^sA>IM7z)Wws1u z+WYS^Be0*^P7;*OGKHT``gMfmQ{=j85bAQMk{{bPr2?HnJzqP#6)cNT-Yz-)tZvzK zf1{WQVy+ndKSnU33V-`{$gct-UB-PQr!|0Lfjrr+N+uTJ$-*e$Wu zFB?G#hnatG1U<)A3ppGI3=*LGhnP{8935C1q>|OAzWw~{3ep$@I_M+XWt(iz#)G02 zvttA`%QnR+vnv@ffObrw=u(KyJheB76SmK;pq&LY00mdYiIys&guL-MnL8U1r~oXi zpT16!<1>{8OxhQ<(Z{CRNBcL^3+$RVNJ`2(wg>-^6h#ZHXSfpCC0&k{1#N*?9qq{& z;SpkrP*+g^@r)d0TJH=0aF(SUo>^~4NAie~CLpK3%M2_0C&ys*+`Y41}Or6~O-&@G7M31>B&o^I%idR@Ql3Ozmo8d-u)yINF9zGCQ6X zTh0y^E7ks*eONkF`~8#NCBK@%=qSd6$8$k%YES|Kp-znqaujS#_lI)e82znjcYC2C z#}jgUui`8(^xkZC(6Q~UmIhY#MF+v_vQ$4u;raE*UFTX&n2dZ$NQQm4LsDLlEiU&0 zX}Xdf9lA8X=AQV&rSwNFCHB~Y=mv*U$nt&v&4Cmb(W&-WfzteNGiLeMRNq#=r4+nC zrXPNiWMQ5sbLbxiRZH97%BM`_S?b; zhI+PU)O1=}VRAT#l;3aVxCF9=Rz&90BZIvD?u5}Z3^mpVr4A#DbF zNf)cYtN<7PH3;y*jv)Nj8=Ru~3E^X*J)T@lWW4;LbUZn~&89mKRA~t`bE-YDjpEpo z9r!gT{C=2*roHNl-=EUFZK;XglJ@q^=`lTOxY(~;y%f^kH5yqo?}Pu40&Z7>se~y% z7wvE^1u=QI1pu_OAUKA+0f6*B4#uu_~=}su3t4mnMrm8d5D?tbTV&-*6Vv?S^vs+AZjldq4?*Oj3XxZv_DIS@-H< z#w;K#xqxy%Jq7=uI3bbtFteGLUTZviGp&^6!)2PixS1KZw2BqI+r1aa=Wb5VFo`%Q%XS-NvvYKLFw#hTwz2|KO zHMr3JGsk`#T>N#}n0hAVvC38=-PmkvC;>kBLXr6)tt{!@7kcCdsuG3u(fi?6ml50!?F|507fj7^ya~ihz%UFc zsBj+u0R!{ZR{f$jx(C2GIlssGn?-aGYC|AmFcY9WkDOccZ&9kkRWOM+h^OLCt9?r# z{Pmi>JdXC%+x_u}3NeaKRtL=+w2Tr92)6%)=s>mkENwFBm~FG6E%O|;l~k(pA2;P*ckZR%a`zy0i;6r4b8kuAN)%)&4?~ZLbAZ__Vmyqv(XhIAm%p?eP0 z5Nt=VUs?P$$bfwH02#jolAh!Mkkae6ZOAYeGClVnt&dHl6)4Z)KyPlJ4{2C{Tf#zu z+kN|LwS1^93lIu1{Ajqlrzn(ggcehVZFuK+`5t5>_ZwH1JDb!6W&x}$uxo2+x{C1?E|db)*-^Y0|!L> zhYQQUR??$iLxJNIz$H!pl?I>|{RTsSXZYt>On=$Hr9FR6An3=_Ki5F|E5D!X`EWd{ zk6RlYbYbiG`-94VyfoVD0se!u#j$ezgx&YY5oCN2?PGB>1aL=~Mo_Ec80J?iP2L2A zxF0enhxE?5sR4Yu3;8}8lHlN%!$w1`^rcR2+(0HKk)D5FPOw9r2aQXd6!jf$*ViuR zdLE#%kTb%D8khbVz?BY1pdgcP+}m9QpG1a4k+wBA#JLNh6+3Jeet!Z+<_6IQJa-TS z#5p8?!U2Wv&%AIP6rDZcPDOwrUIdc-xOa@1Fi3r4CJbB`V#xn-CM>(%(CCz*LyK~7 z14#0-{^_HZevaokJnrjOKsFD=M|3jd=R{735fPZr#pcMNe@?WAs2%Adp^RMw65@VN zhJx4}iRK{j!R}Ef5|)2XNb1W#T+oK3lAona4nNC9$}s)?oY3FbUjZ70U7{XDi9aV@ z^%1b#3I%}~KPAQX75YG9i&so5CxaZ977a+MB5l>fuipXbz{{XsO|^!`^N$WcMxMN zP>cnNu|P5OZJhsynKo7_d_4_0l|&|d+(VBnrO2@bP7inw3bz}K@Q&cMnET1ee zqD7gECEBbYo!{ktlUbEWZ|1wt`@T*%VAjVY{{Z)^&k!Qi(Y*=h>=GIhyQ*;HD+At+ z!5KnAXkSowgQ5NYakX#gLtHGj|I0nDmo>`=*@!CGhWA$*@V=a%WK%C|hQnL%%6|{U^>+Q!EiX;EL zOWs9E$eyrawOmzIJ{VgS|I5fZb-b-~o4or*?J@zI>8vh26S_28gZ&cAuE4VMLjsmnn)90qrIg0LBc3&Y<| zRpEad@7rbjEOk63>2!;W<`Gt=y#%|C!tTL!OR}-75MunBKX2(=PVlqEi4+d}1UR4L z&Zm9nT#~i;miS-QGtec$z}3`BZLoPsWcC9shwLYBCrfx1ylk9J^@$3?mvU9mKC7N|JGtMSUzq7x}+Kz9D*Ng zZY|Xey7v5Uqmm4q!HG%Ae&Q`t6DBbhaKzDyW zpjw;I)0KLe^)KfJ55l9z-tpnijvUF;+0|?&F^BCwk*4R|@wZj~QGbq9m}zgpof5~_ zZ)@zo8&vp%5B)bm^omFwSeQSd|FpxIYrE9y*Ol9$EqZ$Z`Pom*zxWCK&9Sr}vYQ$s zoy)0z!llH7fnd9S3;p@JY}VWX%&&Q#7N4>7&+)9kNnFbgzXq*XEJ7&PwBMS+Z9oIM zUqH}85YL6qlM2qjbslBls9Z~jw(ICzuoYLaU1hZ*coM(hBhZ(ozT8i@KwHYXNU(8u zlHI;|@HZNlV8{Xe_>OlS&*2Edrc z413J5#|(S4d2B3V1MtF|ea9j;2(i4FVJu>Ekn{gbDAETvxiAyKZa*tS7O6Sf?Pl1c zkQxEV5K@d5>wg82@E~XiViAnRh4#GZd^l+Tl}yHiav?%QKqnW;Blt$z0!Oc1Rofir z+ckJASPOkcqntWmSdqh;C&4d{)Z2cey{jmrhZl8296O*`7&gfAR!k7QAb5xi=wW>T za9GSs6h@D3^#RoyPd)bk)7AS#VlRjmS-g5>8h|2mc%;c&A@G!}2sDJ)nNvpf6-Qi040MHv&`;(07MAgStqN$ll8 z43Ns?7LwpUj)}wpgvLQq5t5^ZVG3~z0Jk|zL2;r<6eqWWDaTZ70`xy{YoKO|D{g1Q0x5|{rS6DiUM_#P`A6De;CX3R~z;HS*ToLrSQj zy+uxv=B98okrg^yBE7!wq|H3J!vSRt+8kHIW+eFFz%qVFitG!K@4nd_bu_ zQ8KDX$xZpANuWFg1Q!r}DXR4$6$Ai&IjX@KAEbKc3?6+c`t;>{IC=Gz=)>*maADqglZ@}k(0otfk9kggwP ztmG|JM~s!+Vrul_F(;K>l5tQ6@mfVoiEtw$C>TMqqErhlGyo)&M@l;vQI@KTA>xlC5f zc*faJ=x8s}MMwNzECRekw!gXt-T%I}N&g(^@Q1dA69GthJCZ#9zxJ*@s;MjMw{&;K)aKbhhGp{-utsSE(Ofg_C_ zKySR_EW!Q1*1DYw1I4%~#R2}dB4i>;8ZNaFPj;(>&IRy7k1lctj|XB!l;;3{ zKSZ)rc2%8J9FNk9w&E9*ZGv`e^ax|xb`Cfd4=4dRhuO}y*g41vQ@X*Spy0mo>9yBD z+ed~vS(5kHZiU;>s%Lat0BS@4b@z0}_|P5OAyCGnO^8;uIPM!)_-F;n&DlqWhT`0% zH^DzCMfV|0G$cvK9Oan~A`h5Yl=g^&kOg8(5&*+DaXSJkA}~T^WGKrZUvyV+5Zy6g z+Zo_6YVSDerNPiqehH(qaUV8CwlT%P_)V;Xji_~O(}s*!(TnnI68N?Jjw(hyK2Dr(AxZI^b+W&of`(0cWUF~ z7H5jm+l%2WV~t>?Of$W}zB`xUcxd9Z1y1{m7{ST9WAD1%Y24Qs0KeqaX8kyQZfCm2 z%lz$T0OcUPo2BPrvEgwSb5pG9hf93Irs7+RF> z3?cw1M+3Ks4l?6CM>=Nevk33$$jUl*vmzE9P4j}_9szV{v<9L!XJ}dTJ4YgB>a$2` z?iBZc_#X!lK>$9osdHeWYl(W!fpP9W6V?C36MrRcdH0>aPHD#;(Ag5y&K07ei}=yEEa-aC;{0 za_^3fbEq9xXxIJjHgB^U6kA{p&D>yRfw9pVY2fIPR#@r*AC?z?^bl2l9QFPf{^-!n zHM!tFeuoX=tfO#mrd7qa(C2*zvyV0+vEKEtw$cnS+X48QLt|xj2|tFb z`C#YTm^e_<%>s9uuy#mP%K3SUHDo!ulYqZ0vP#enL6if*bpeW*{PV zWWdB(VpUUY3Tn*^|I`Bpn~#w2#C!wsMJ&P|)B!r*q2}884W%2c`#|x7QDO(bG{oCF zyLTSm{!A}sSxqlo`Lrd9-=XA%4-u_}{LvWNup8WmGncIZ$Q?Lc%qiOe1#{rZDVA3{ z8(ue3>-g{hyqz|(8P+PTgK*27FZE{56&jh}8D*r?%zS-}@vJ4vnhh7@mZx}secw25 zW=^ji*Z#`zZcn)TTo$mAE?^zPgPAm)#=JVBnB>0|@=n-$ZcLCbFFu8q2<)^1IBOEnS(E0^kzu-rhj=iD@i4`kP{e?w1wgVA-P?eC^UP;t z!bx68o)qD-&@|iZx{A!3Bh!aw@PK|kfMR>dG^09ipF1?gHM=E(Oq)aR(X1b$o->~1 z@dAT6nGaNIc6p%5)Y(}VL~4!#6*Nyfs|A8e&93!me)GY!q(jKl85cz19TsC=>5#($ zoy7PQ7u~WY70XVlGSOFoG&I6 z*dkSJ4~=*tk@Gce2kIio!WlMAc`<#W4_!*Y;GjDSohV?M1qS3eeTvgz*3lAYG(exy zjY&Oxc(jQYVUJhJIA?Lni+MpIRxF44IJ-s~mHDN6Bvxli3ntuGT|;7-g~ZQj zYJI*$x@XqyIlebz(bflaEq>w1N15amPwKM&5fbq1SftWuKJxQ)LzuoPcI`^-I?nI+ z)cBNoxqP7P31Z>G(jO#^`!}`p_5v)F^eZ9Zk0YHMd($)2x*G={Du^jfRXmkmdxLlS zBV+xK1@50_a0Vz=|Gq6pr`$Jnjwang*yCo%X?|`k;H9$`lnj7b@hA+j&E=%~?myj* zTiO@(KIx$gdF+!B5?q2v3E|ukv+MXxPW_1xH@>bopR<1=;?yqq3D|<#64frN(#90T91(f;D!Zuyu`zXI0WDAeZ!c4TYat3as5~34k;Tg=M z5ObZBcWBcr9K*)S_PLZC!-uqWi*naNj5PdEPPx#p7^rZdr8`^J$ zv;pMiB>R;o7V+NDDI0(=g;kDAhC*-+FU0KQy}rPHO}i`dD4ZeP#L}Kd`gOrnu_gfr1Z+3 zhiWyqMXwccTP`s}=|zRUqXgDsvj|*zCbu=EERz&%8es>l4dHdwMX6t6EKe&druoK` zG_i-nRUzy7f;UD`m|*_`^CFTr zX_M`;q^nvtB9I-#ei5Z=-Nsv1!A-143ykl!veApzGO9i!?Md?qfQJ)l4HU={96I>z z;!vhbeef>moJQ4YcB70DtkGXy6K&%uZwC0%)#tYBRG3jK7;RJDY?56ox;; z#a0Iyo8ZovtheLhl*3JCL5VxnYG#44GFJsfu&RRdU6rtAtVk;H1Wi?FJeKgsSjLvu z_&>_)i3b7h=TO0<+A-3k-1& zTC6)E%n-IAS0jrl-b4oSuyIwv;X5LkG*riM(cvlS&4Loa+7rz5l*(*UMq0{HFQp${Ao*)_OHD-}A#LR(e zhnrqL!CaowZPZp}5iK!_S2~&*yUCZnp#_jCQ`7W~f)X4eG5PR-r_v$`b|3StEA21u zwRS6od8R9;+-_L6mR5Kz$Hbg3KTfh5Z5O^C6IJp}l<3oKZ!47kA;?1}NYUjg&Xz7$ z*Q?G*uStsvSDv;oI#U>9IaL9KFe2}L;|@hax9T=)3-upf%v4fRWUgBt#7a%*K)cYN z*gRom{v$D8E+qf?_ZKF{0S_*aEb*GPYYFT)@}ZMU-8b}Hj2b22T_CUoWcBM^s9JU0 z^__D0Rqjx{A-+L#hZktOHEMs{-Ed=zHamdj#wCjd>&_(IzEYII;|8t~WrySBSeFD( zk0jE7VBHQKrP(2kjP!=x`!3X;@Oiu2_}ih;EG|{|1+$91!*H&@JRvgQ+pRjiWA9-F zmS9Lqc%I*;7OcF)XYt-=mGjdHV)@CE(}FWy*F}16A*sE!q)1ODoQ`PoqpO9YmqP=3 zf0m6zLA4k7Zcwqk@D1RaLlVT538ZHC0$+?> z6!E9!d7esFN|3RWBM{>hfJc|d3;)Lwbv+?p(5mr`4d*UD#HVV)s5)4aC}D-+2+FrF6BoVsX%zK zLSDmcLu6nqp)|^*;L3S{qOG^Re1S3yYxKrsR#tY^E$-2NySOJX&y(QQz)SO^RQj{x z4aB^_>aL6aBK5`dV-ZY(x_%R`W-xw019vu3T#Fc0BQXQe4smwT=KR%Iia49-kra!i zFKj#9!dstWY=;!AfDEg-s&JnD9Xe&&?!8R7P>r#NT_B2Q&8W z@;xan&LeP+<6lxg?+TyVZWA-_4MC-AVVz9nv&NF@Lnnr}`LL4x>hAQXe2A!Gi-s*bUcm2)k`tqSOyq=3#(dLq`nX8y*q17I8`Pc&dN@^_j^C)%f zCbcP?(L-kx?G6*Q;zJmFx6xM>ST&v1^c3*Muk7=5!f0f%P`_NqPtnF(>lOn6Zy-RX zY{Yoh3yfaA7xtZrALv#TWoxVqWm@V$)-vkfx!K4fP<6Z4tRv#|`;SM(#PuK+VUzI_ zCLIj&0LevNZ!u}kjb#_9x4NlW!iyJ{UDq5JhvEu^s^xA(M69BYHfrS$8uRkmrbS~n0VPpwFd#eQ+leZ!&tJ^dZwTymo<&8iy?kcw8(Lkpn= zyF?&hV-w8l&QH(;+T1g9m}q|5OUmhA01T(Xv8RlOX_*dp&W@JvxlSZP=| z{$vtZ3KyG6gTvRi>xlV^AWo#QSrW62gi+V)RA#k1R*$r=Jk;Nd%zgQgVyxBUbjbxl zB%UC^NXVrL`n;kTEGv&-OkxKzdJ6Xuw?&fChExB1JSw`tht&0_u$4lUrgb-)9XUSo z0Y#3kusl4X8jPG&me2*=%GwNE-7;oPKEE>Uk~lYRp|eFOrhX_s7cCci&= zb!5Pb0&kKP!o~ZJG`)5xttNwpWAtF$$o}AMQNUsOUT^gs5ZA7BmB6wqI-@%4?E^d3 zg)F0v){(5%kpS(nEnuaW3OvO?I2<7k17;l`LK)hfbpc9jfa1i+y6EZ-D0Jg3OF#5w zc7B{v-hvu`a)+f_;V<5B-t;A~gIRe)ilSD|(_lWEM9HkkzzCt<=mnk9b&Iqzv#eGY_)IP{+k5mZyMR=& zo>L&+iigmH-@of9{v+1$VNgbRqE21w!-ToP{CE8a_*? zF9M6x*s?a1u^f}4Yh`#7XkD?rjr`@&&(R~Q5(06oWS!Pn4w+@uaNpV!IlDKE4uZRM z5>cyKrcv#qtrh-`o!pmbNgRx|P!C{V3qe|Yrt;@2QoZzAk)AtrRr4L2=O_HwH7K>)tP66J!jM{^jBUehfDFbXdQwJicJL^4CNgblscWfxv*XObD zRb&G&F8|(pxN=zme&04yvm%0vf$wM*5CL>Df5r@bJFdTXm@<@TfQXiMnB*JGy3Sdf zLQcaPDFzofi>WfMZ^D!t>UZmKFH?JHNyrT*=tVruHvqnK%2+E^vs^(JJlE7;{1^F7 zc)pMB)_N~>6wimXA;i7FEe4>j^W~75}C_2To{-gzPz`a0N4~<0hK_EICR(FRpvXt2FMv+-{(7Z^J5`TRyz!+s}mRRCa zTI!XKiGk`hT8mlJDf%v~O$aWYXGR3INx2CfiH6}4LP`Xe!E)Qb4WIm>SePwUk(0#2 zB0?4+4pJ`w5Tu3KhBz*ML~wZJD>-qH8@>a8FiKgOu3M(%CHfiTt?><_*QqC%D|kOE z1*SyZ7aHwmmOP=N2l@^Fn*2`DhxupE2LNeg8-LJ5Q?Xz8frvRtv`IbuDT7c5K9D0H z)R`$hCz#As{x=|?OI7%w!tlYdgdekkYsRP}qqZsJelBsxfjeX2qqmU;;x2;#a-|q< zrzKPzip=)~NcDMjTqFVufon7VOCZ}$(9t&!?haO7T_zar@01ka)N1KYc~)PLuoH(L z$OTbGI4d(SJS54Fmij_NWy{Tg_)7^LvHs0!k}ClpA@W#xt-b)54^Nn!)@+e^Ehx+) z0Ne!H09dju&$m#3Z*n^e*}tetc&etQC4dwd%uy`HF>vyPOJd@YGA^k-)>2HT1W|Cb zv<_Jyzd_z5{V;!g=&@AnsRKYJvbMaS!Ucs;Cq;}Zj#4Oz8QRUOV&Ya7AgdxH2nVo3 zTyh_DOVuqCidG@E;(MZ6?Tdg;fE#?JNQ8lAe6@