From 528bc7651fda77e7a169a6605b606596c07460d7 Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Tue, 5 Sep 2017 20:39:52 +0200 Subject: [PATCH] wip: Evaluation --- .../Evaluation/EvaluateAlgorithms.java | 53 +++++- .../Presenter/Generator/DatasetGenerator.java | 6 +- .../Presenter/ImportExport/DataImporter.java | 3 +- src/main/java/Presenter/Presenter.java | 35 ++-- src/main/java/View/MainFrame.java | 32 +++- .../java/View/Panels/EvaluationPanel.java | 168 +++++++++++++----- src/main/java/View/PlotDialog.java | 81 +++++---- src/main/resources/Thumbs.db | Bin 13824 -> 13824 bytes 8 files changed, 263 insertions(+), 115 deletions(-) diff --git a/src/main/java/Presenter/Evaluation/EvaluateAlgorithms.java b/src/main/java/Presenter/Evaluation/EvaluateAlgorithms.java index 9fb4871..4fa9748 100644 --- a/src/main/java/Presenter/Evaluation/EvaluateAlgorithms.java +++ b/src/main/java/Presenter/Evaluation/EvaluateAlgorithms.java @@ -3,6 +3,7 @@ package Presenter.Evaluation; import Model.Interval; import Model.Line; import Model.LineModel; +import Model.Point; import Presenter.Algorithms.*; import Presenter.Generator.DatasetGenerator; @@ -10,6 +11,8 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Observable; +import javax.swing.JOptionPane; +import sun.awt.image.ImageWatched.Link; /** * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden. @@ -25,6 +28,14 @@ public class EvaluateAlgorithms extends Observable { private Integer iterationCount; private LineModel arrangement; + + private LinkedList lms; + private LinkedList rm; + private LinkedList ts; + + private LinkedList nodeLms; + private LinkedList nodeTs; + private String[] lmsResult; private String[] rmResult; private String[] tsResult; @@ -63,7 +74,7 @@ public class EvaluateAlgorithms extends Observable { public void run() throws InterruptedException { - for (int i = 0; i < iterationCount; i++) { + this.arrangement = new LineModel(); DatasetGenerator generator; @@ -75,15 +86,24 @@ public class EvaluateAlgorithms extends Observable { } arrangement.setLines(generator.generateDataset()); + setChanged(); + String[] msg = {"eval-dataset-generated"}; + notifyObservers(msg); IntersectionCounter counter = new IntersectionCounter(); counter.run(arrangement.getLines(), new Interval(-99999, 99999)); counter.calculateIntersectionAbscissas(arrangement); + lms = new LinkedList<>(arrangement.getLines()); + rm = new LinkedList<>(arrangement.getLines()); + ts = new LinkedList<>(arrangement.getLines()); + nodeLms = new LinkedList<>(arrangement.getNodes()); + nodeTs = new LinkedList<>(arrangement.getNodes()); + + lmsThread = new Thread(() -> { - LeastMedianOfSquaresEstimator lmsAlg = new LeastMedianOfSquaresEstimator(arrangement.getLines() - , arrangement.getNodes()); + LeastMedianOfSquaresEstimator lmsAlg = new LeastMedianOfSquaresEstimator(lms, nodeLms); lmsAlg.run(); lmsAlg.getResult(); List errors = sampsonError(arrangement.getLines(), lmsAlg.getSlope(), lmsAlg.getyInterception()); @@ -95,7 +115,7 @@ public class EvaluateAlgorithms extends Observable { }); rmThread = new Thread(() -> { - RepeatedMedianEstimator rmAlg = new RepeatedMedianEstimator(arrangement.getLines()); + RepeatedMedianEstimator rmAlg = new RepeatedMedianEstimator(rm); rmAlg.run(); rmAlg.getResult(); List errors = sampsonError(arrangement.getLines(), rmAlg.getSlope(), rmAlg.getyInterception()); @@ -106,7 +126,7 @@ public class EvaluateAlgorithms extends Observable { rmResult = getResults(errors,perrors); }); tsThread = new Thread(() -> { - TheilSenEstimator tsAlg = new TheilSenEstimator(arrangement.getLines(), arrangement.getNodes()); + TheilSenEstimator tsAlg = new TheilSenEstimator(ts, nodeTs); tsAlg.run(); tsAlg.getResult(); List errors = sampsonError(arrangement.getLines(), tsAlg.getSlope(), tsAlg.getyInterception()); @@ -125,7 +145,7 @@ public class EvaluateAlgorithms extends Observable { tsThread.join(); createGlobalResult(); - } + } public void createGlobalResult(){ @@ -145,7 +165,24 @@ public class EvaluateAlgorithms extends Observable { setChanged(); notifyObservers(separator); + //visualisiere m,b + ArrayList lines = new ArrayList<>(); + lines.add("lines-res"); + //lms res + lines.add(lmsRes[0]+""); + lines.add(lmsRes[1]+""); + + //rm res + lines.add(rmRes[0]+""); + lines.add(rmRes[1]+""); + + //ts res + lines.add(tsRes[0]+""); + lines.add(tsRes[1]+""); + + setChanged(); + notifyObservers(lines.stream().toArray(String[]::new)); } @@ -247,4 +284,8 @@ public class EvaluateAlgorithms extends Observable { return sampsonerror; } + + public LinkedList getData(){ + return arrangement.getLines(); + } } diff --git a/src/main/java/Presenter/Generator/DatasetGenerator.java b/src/main/java/Presenter/Generator/DatasetGenerator.java index 860f8ca..9e56a0e 100644 --- a/src/main/java/Presenter/Generator/DatasetGenerator.java +++ b/src/main/java/Presenter/Generator/DatasetGenerator.java @@ -28,15 +28,13 @@ public class DatasetGenerator extends Observable{ public DatasetGenerator(){ random = new Random(); - random.setSeed(9999); - m = 1 + random.nextDouble(); - b = random.nextDouble(); } public LinkedList generateDataset(){ LinkedList lines = new LinkedList<>(); - + m = 1 + random.nextDouble(); + b = random.nextDouble(); for (int i=1;i<101;i++){ diff --git a/src/main/java/Presenter/ImportExport/DataImporter.java b/src/main/java/Presenter/ImportExport/DataImporter.java index 73ddc00..8aad5a0 100644 --- a/src/main/java/Presenter/ImportExport/DataImporter.java +++ b/src/main/java/Presenter/ImportExport/DataImporter.java @@ -62,7 +62,7 @@ public class DataImporter extends Observable{ setChanged(); counter++; result[2] = counter + ""; - Thread.sleep(20); + Thread.sleep(1); notifyObservers(result); } @@ -72,7 +72,6 @@ public class DataImporter extends Observable{ } catch (InterruptedException e) { e.printStackTrace(); } - } } diff --git a/src/main/java/Presenter/Presenter.java b/src/main/java/Presenter/Presenter.java index c6f0465..a6e9617 100644 --- a/src/main/java/Presenter/Presenter.java +++ b/src/main/java/Presenter/Presenter.java @@ -53,10 +53,16 @@ public class Presenter implements Observer { public void update(Observable o, Object arg) { String[] result = ((String[]) arg); + if (result[0] == "eval-dataset-generated"){ + SwingUtilities.invokeLater(() -> getView().addEvalDataset(eval.getData())); + } + if (result[0] == "eval"){ - SwingUtilities.invokeLater(() -> { - getView().appendEvalResult(result); - }); + SwingUtilities.invokeLater(() -> getView().appendEvalResult(result)); + } + + if (result[0] == "lines-res"){ + SwingUtilities.invokeLater(() -> getView().drawLineResult(result)); } if (result[0] == "lms"){ @@ -301,26 +307,12 @@ public class Presenter implements Observer { } } - public void startEvaluation(String[] args){ + public void startEvaluation(){ if (evalThread == null || !evalThread.isAlive()){ evalThread = new Thread(() ->{ - Double m = null; - Double b = null; - Integer i = null; - - if (!(args[0].isEmpty() && args[1].isEmpty() && args[2].isEmpty())) { - m = Double.parseDouble(args[0]); - b = Double.parseDouble(args[1]); - i = Integer.parseInt(args[2]); - } - try { - if (m != null && b!= null && i != null) { - eval = new EvaluateAlgorithms(m, b, i); - }else { - eval = new EvaluateAlgorithms(); - } + eval = new EvaluateAlgorithms(); eval.addObserver(this); eval.run(); } catch (InterruptedException e) { @@ -330,11 +322,6 @@ public class Presenter implements Observer { evalThread.start(); } } - - public void stopEvaluation(){ - if (!evalThread.isInterrupted()) - evalThread.interrupt(); - } /*************************************************************************************************************************** * Getter und Setter Methoden ***************************************************************************************************************************/ diff --git a/src/main/java/View/MainFrame.java b/src/main/java/View/MainFrame.java index bfbd4be..0a931af 100644 --- a/src/main/java/View/MainFrame.java +++ b/src/main/java/View/MainFrame.java @@ -1,9 +1,11 @@ package View; +import Model.Line; import Presenter.Presenter; import View.Panels.*; +import java.util.LinkedList; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -105,26 +107,26 @@ public class MainFrame extends JFrame { public void visualizeLMS(double m, double b) { plotLMS = new PlotDialog(); lmsPanel.setPlotDialog(plotLMS); - createPlot(m,b,plotLMS,lmsPanel); + createPlot(m,b,plotLMS,lmsPanel, "LMS"); } public void visualizeRM(double m, double b) { plotRM = new PlotDialog(); rmPanel.setPlotDialog(plotRM); - createPlot(m,b,plotRM,rmPanel); + createPlot(m,b,plotRM,rmPanel, "RM"); } public void visualizeTS(double m, double b){ plotTS = new PlotDialog(); tsPanel.setPlotDialog(plotTS); - createPlot(m,b,plotTS, tsPanel); + createPlot(m,b,plotTS, tsPanel, "TS"); } - public void createPlot(double m, double b, PlotDialog plot, JPanel panel){ + public void createPlot(double m, double b, PlotDialog plot, JPanel panel, String name){ SwingUtilities.invokeLater(() -> { plot.clear(); plot.createPlot(getPresenter().getLines()); - plot.addLineToPlot(m, b); + plot.addLineToPlot(m, b, name); panel.revalidate(); }); } @@ -141,7 +143,7 @@ public class MainFrame extends JFrame { evaluationDialog = new JDialog(); evaluationDialog.setTitle("Evaluation"); evaluationDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - evaluationDialog.setSize(800,500); + evaluationDialog.setSize(1600,800); evaluationDialog.setLocationRelativeTo(null); evaluationPanel = new EvaluationPanel(this); @@ -165,6 +167,24 @@ public class MainFrame extends JFrame { }); } + public void drawLineResult(Object[] res){ + SwingUtilities.invokeLater(() -> { + Object[] result = Arrays.asList(res).subList(1,res.length).toArray(); + evaluationPanel.drawLines(result); + evaluationPanel.repaint(); + evaluationPanel.revalidate(); + }); + } + + public void addEvalDataset(LinkedList lines){ + SwingUtilities.invokeLater(() -> { + evaluationPanel.setDualPoints(lines); + evaluationPanel.repaint(); + evaluationPanel.revalidate(); + }); + } + + /******************************************************************************************************************* * init GUI ******************************************************************************************************************/ diff --git a/src/main/java/View/Panels/EvaluationPanel.java b/src/main/java/View/Panels/EvaluationPanel.java index a0d0adb..ad8ebf7 100644 --- a/src/main/java/View/Panels/EvaluationPanel.java +++ b/src/main/java/View/Panels/EvaluationPanel.java @@ -1,7 +1,11 @@ package View.Panels; +import Model.Line; import View.MainFrame; +import View.PlotDialog; +import java.util.Arrays; +import java.util.LinkedList; import javax.swing.*; import javax.swing.border.TitledBorder; import javax.swing.table.DefaultTableModel; @@ -15,61 +19,55 @@ public class EvaluationPanel extends JPanel{ private final MainFrame view; private JTable table; private JButton start; - private JButton stop; + private JRadioButton evalTypeOne; //1: Alg - N: Data + private JRadioButton evalTypeTwo; //N: Alg - 1: Data + private ButtonGroup radiobuttonGroup; + private ButtonGroup checkboxGroup; + + private JCheckBox lms; //1: Alg - N: Data + private JCheckBox rm; //N: Alg - 1: Data + private JCheckBox ts; //1: Alg - N: Data private JPanel comp; - private JTextField iIteration; - private JTextField iSlope; - private JTextField iYinterception; - private JLabel lIteration; - private JLabel lSlope; - private JLabel lYinterception; + private JPanel algorithmPanel; + private JPanel leftSidePanel; + private JPanel datasetCount; + private JComboBox datasetCountChoice; + private JLabel datasetCountLabel; + + private JSplitPane splitPane; private DefaultTableModel model; private JPanel buttonPanel; + private PlotDialog plotDialog; public EvaluationPanel(MainFrame view){ super(); this.view = view; this.setLayout(new BorderLayout()); this.setBorder(new TitledBorder("Evaluation der Algorithmen")); + + init(); addComponents(); } - private void addComponents(){ - lIteration = new JLabel("Interationen"); - lSlope = new JLabel("Steigung"); - lYinterception = new JLabel("y-Achsenabschnitt"); + private void init(){ + datasetCountLabel = new JLabel("Anzahl der Datensätze"); + Integer[] choice = {1,2,3,4,5,6,7,8,9,10}; + datasetCountChoice = new JComboBox(choice); - iIteration = new JTextField(); - iSlope = new JTextField(); - iYinterception = new JTextField(); start = new JButton("Start"); - stop = new JButton("Stop"); - + evalTypeOne = new JRadioButton("1 Algorithmus - N Datensätze"); + evalTypeTwo = new JRadioButton("N Algorithmen - 1 Datensatz"); + lms = new JCheckBox ("Least Median of Squares"); + rm = new JCheckBox ("Repeated Median"); + ts = new JCheckBox ("Theil-Sen"); + radiobuttonGroup = new ButtonGroup(); + checkboxGroup = new ButtonGroup(); buttonPanel = new JPanel(new FlowLayout()); - buttonPanel.add(start); - //buttonPanel.add(stop); - - comp = new JPanel(); - comp.setLayout(new GridLayout(0,2)); - comp.add(lIteration); - comp.add(iIteration); - comp.add(lSlope); - comp.add(iSlope); - comp.add(lYinterception); - comp.add(iYinterception); - - start.addActionListener(e -> { - String[] params = {iSlope.getText(), iYinterception.getText(), iIteration.getText() }; - view.getPresenter().startEvaluation(params); - }); - - stop.addActionListener(e -> { - view.getPresenter().stopEvaluation(); - }); - - String[] selections = { "Approximationsgüte","Theil-Sen", "Repeated-Median", "Least Median of Squares"}; + leftSidePanel = new JPanel(new BorderLayout()); + comp = new JPanel(new GridLayout(0,1)); + plotDialog = new PlotDialog(); model = new DefaultTableModel(){ @Override public boolean isCellEditable(int row, int column) { @@ -77,21 +75,105 @@ public class EvaluationPanel extends JPanel{ return false; } }; - model.setColumnIdentifiers(selections); - table = new JTable(model); - table.setDragEnabled(true); + splitPane = new JSplitPane(); + algorithmPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + datasetCount = new JPanel(new FlowLayout(FlowLayout.LEFT)); + } + + private void addComponents(){ + evalTypeOne.setSelected(true); + checkboxGroup.add(lms); + checkboxGroup.add(rm); + checkboxGroup.add(ts); + evalTypeOne.addActionListener(e -> { + checkboxGroup.add(lms); + checkboxGroup.add(rm); + checkboxGroup.add(ts); + }); + + evalTypeTwo.addActionListener(e -> { + checkboxGroup.remove(lms); + checkboxGroup.remove(rm); + checkboxGroup.remove(ts); + }); + + radiobuttonGroup.add(evalTypeOne); + radiobuttonGroup.add(evalTypeTwo); + buttonPanel.add(start); + + algorithmPanel.add(lms); + algorithmPanel.add(rm); + algorithmPanel.add(ts); + + datasetCount.add(datasetCountLabel); + datasetCount.add(datasetCountChoice); + + comp.add(evalTypeOne); + comp.add(evalTypeTwo); + comp.add(algorithmPanel); + comp.add(datasetCount); + + start.addActionListener(e -> { + view.getPresenter().startEvaluation(); + }); + + comp.setBorder(new TitledBorder("Konfiguration")); + + + //Tabelle + String[] selections = { "Approximationsgüte","Theil-Sen", "Repeated-Median", "Least Median of Squares"}; + model.setColumnIdentifiers(selections); + table.setDragEnabled(true); JScrollPane scrollPane = new JScrollPane(table); scrollPane.setWheelScrollingEnabled(true); - this.add(scrollPane, BorderLayout.CENTER); - this.add(comp, BorderLayout.NORTH); + scrollPane.setBorder(new TitledBorder("Ergebnisse")); + + + //Plot + plotDialog.createPlot(new LinkedList<>()); + plotDialog.setBorder(new TitledBorder("Plot")); + + leftSidePanel.add(comp, BorderLayout.NORTH); + leftSidePanel.add(scrollPane, BorderLayout.CENTER); + + //Splitpane + splitPane.setOrientation(JSplitPane.HORIZONTAL_SPLIT); + splitPane.setResizeWeight(.5d); + splitPane.setContinuousLayout(true); + splitPane.setLeftComponent(leftSidePanel); + splitPane.setRightComponent(plotDialog); + + this.add(splitPane, BorderLayout.CENTER); + this.add(buttonPanel, BorderLayout.SOUTH); } + public void appendData(Object[] row){ model.addRow(row); this.repaint(); this.revalidate(); } + + public void drawLines(Object[] results){ + String[] castedResults = Arrays.copyOf(results, results.length, String[].class); + Paint[] color = {Color.ORANGE,Color.ORANGE, Color.RED,Color.RED, Color.MAGENTA,Color.MAGENTA}; + String[] name = {"LMS","", "RM","", "TS"}; + for (int i=0;i<6;i=i+2){ + Double m = Double.parseDouble(castedResults[i]); + Double b = Double.parseDouble(castedResults[i+1]); + plotDialog.addLineToPlot(m,b,color[i],name[i]); + } + } + + public void setDualPoints(LinkedList points){ + plotDialog = new PlotDialog(); + plotDialog.setBorder(new TitledBorder("Plot")); + splitPane.setRightComponent(plotDialog); + plotDialog.createPlot(points); + plotDialog.repaint(); + plotDialog.revalidate(); + } } diff --git a/src/main/java/View/PlotDialog.java b/src/main/java/View/PlotDialog.java index a4b7c71..0aeb699 100644 --- a/src/main/java/View/PlotDialog.java +++ b/src/main/java/View/PlotDialog.java @@ -30,58 +30,56 @@ public class PlotDialog extends JPanel { private ChartPanel panel; private XYSeriesCollection datapoints; private XYSeries series; - private XYSeries linesA, linesB; private Double min; private Double max; + private XYPlot xyPlot; + private int seriesCount; + + private XYLineAndShapeRenderer renderer; + private Shape diamond; public PlotDialog() { super(); - this.setPreferredSize(new Dimension(1000, 1000)); - this.setMinimumSize(new Dimension(1000, 800)); +// this.setPreferredSize(new Dimension(1000, 1000)); +// this.setMinimumSize(new Dimension(1000, 800)); this.setLayout(new BorderLayout()); + seriesCount = 1; } public void createPlot(LinkedList points) { - try { - Thread thread = new Thread(() -> convertData(points)); - thread.start(); - thread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); + if (!points.isEmpty()) { + try { + Thread thread = new Thread(() -> convertData(points)); + thread.start(); + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } } - //createScatterPlot chart = ChartFactory.createXYLineChart("", - "X", "Y", datapoints, PlotOrientation.VERTICAL, false, true, false); - Shape diamond = ShapeUtilities.createDiamond(2f); + "X", "Y", datapoints, PlotOrientation.VERTICAL, true, true, false); + diamond = ShapeUtilities.createDiamond(2f); chart.setBorderVisible(false); chart.setAntiAlias(true); chart.getPlot().setBackgroundPaint(Color.WHITE); chart.setBorderVisible(false); - XYPlot xyPlot = (XYPlot) chart.getPlot(); + xyPlot = (XYPlot) chart.getPlot(); xyPlot.setDomainCrosshairVisible(true); xyPlot.setRangeCrosshairVisible(true); - XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) xyPlot.getRenderer(); + + renderer = (XYLineAndShapeRenderer) xyPlot.getRenderer(); renderer.setSeriesLinesVisible(0, false); renderer.setSeriesShapesVisible(0, true); renderer.setSeriesLinesVisible(1, true); - renderer.setSeriesLinesVisible(2, true); - + renderer.setSeriesLinesVisible(1, true); renderer.setSeriesPaint(0, Color.blue); renderer.setSeriesShape(0, diamond); - - renderer.setSeriesPaint(1, Color.red); - renderer.setSeriesShape(1, diamond); - renderer.setSeriesStroke(1, new BasicStroke(2.0f)); renderer.setBaseSeriesVisible(true); - renderer.setSeriesPaint(2, Color.GREEN); - renderer.setSeriesShape(2, diamond); - renderer.setSeriesStroke(2, new BasicStroke(2.0f)); - renderer.setBaseSeriesVisible(true); xyPlot.setDomainCrosshairVisible(true); xyPlot.setRangeCrosshairVisible(true); @@ -96,12 +94,35 @@ public class PlotDialog extends JPanel { datapoints.removeAllSeries(); } - public void addLineToPlot(double m, double b) { - linesA = new XYSeries("linesA"); - linesA.add(min.intValue(), min.intValue() * m + b); - linesA.add(max.intValue(), max.intValue() * m + b); + public void addLineToPlot(double m, double b, Paint color, String name) { + + XYSeries linesA = new XYSeries(name); + linesA.add(min.doubleValue()-10, min.doubleValue() * m + b); + linesA.add(max.doubleValue()+10, max.doubleValue() * m + b); datapoints.addSeries(linesA); + + renderer.setSeriesPaint(seriesCount, color); + renderer.setSeriesStroke(seriesCount, new BasicStroke(2.0f)); + renderer.setBaseSeriesVisible(true); + renderer.setSeriesLinesVisible(seriesCount, true); + seriesCount++; + + } + + + public void addLineToPlot(double m, double b, String name) { + + XYSeries linesA = new XYSeries(name); + linesA.add(min.intValue()-10, min.intValue() * m + b); + linesA.add(max.intValue()+10, max.intValue() * m + b); + + datapoints.addSeries(linesA); + seriesCount = xyPlot.getSeriesCount(); + renderer.setSeriesPaint(seriesCount, Color.red); + renderer.setSeriesStroke(seriesCount, new BasicStroke(2.0f)); + renderer.setBaseSeriesVisible(true); + renderer.setSeriesLinesVisible(seriesCount, true); } private void convertData(LinkedList points) { @@ -114,8 +135,8 @@ public class PlotDialog extends JPanel { coordinates.add(p.getM()); } - this.max = Collections.max(coordinates) + 1; - this.min = Collections.min(coordinates) - 1; + this.max = series.getMaxX(); + this.min = series.getMinX(); datapoints.addSeries(series); } diff --git a/src/main/resources/Thumbs.db b/src/main/resources/Thumbs.db index 2f2eeb1d565cee41d996fe02069c2757a0a4132f..b0530a087ab375e4b81f8d654441df7075a38d7f 100644 GIT binary patch delta 137 zcmZq3X~@}-!@`%)zx+-R|7AvI1|ZO!T*czY!3t#k2ZG5Hxiu#LVDX-OnALD{32O@z zH|OSGte+TJ76KJ){=hzieKLpUk4>EIjFa1SD>pX?IWzGxGC?(i4YCo+{YZqjk!*xX=zfDr(aV>46$ delta 1653 zcmV-*28#KBY=CUAYy=K4Q)Wo^4ATJv0000$D3f{wP6z@300960|NoPb3n-KL1WOG8 z|NsC01poj5|NsC0lhFhulXL}y0!#?A`UUa<1pfd3|FiH1mJQD zOE8y{7zm^TFaR*KzX-?yfBKukemnS+;~fJ}@g0tVqa~)LX$9m3`Zwp`(xst{Q$H-{2kqY@~X4`3F~$ipA5V$YSM4HO%Fvc3`)|zY*nfN6oOe2WZ>@r57!=wyxUQE#IQ*!03PBqfh+) z_#^)S3d8>Z%IZJiqfh+)_#^)S3d8>Z%IfmD{72$n9BW2L)UG1Cxa+r0@kXC+HYPX= zs#{t^l}^Dt$2s|>f16MK5g!--0MKQB@FRc3Km5I^1Ke~^8udM$B45d#Xim&3&9sEh~c7dpPj{88-p3_aVp7K?TTDdYb za7NO&K-yZVa;idxWdz_B1CivOB>4OB4?wll=3D&^S+wg(?o|$#a*H!T8%psb{{Vkz zx6GkHVimUJoE@ZBx%_tj0Fqhazxx5|U;0ZWZBmq}yGdEJn!jl)r}JOc!}|5<`J>JM z0B7X7c=YjSByq|A00>uui83LO7{lM0R4Tk=JH{O&eZ)rWvNUq4n4f|8X{OpQgSQ0D=F%WTNo zmR!0tl}RNnBE9J~x{Q74HQm{0zSFYmYY0kC(7JmzpCYq%_vLhy8)-|K^0a^b?F~N$ zcq_v`1cpx)>GSV_?U{4-JwDv+EUk^!cDAyXEP+Bag$o`~utrwtzAW(;tta+Xx`92@ zd8=^BK#CS0HpL)s5wK-fCk%GrHtd5-(N^okBFj;_f@orQxDZDm+2)cpG84ixQT*epjJah^fNcGSh;@VYKqp3~or z+LWQoX;X`7TbV2SXl*3ouB^^VxXKhXa#EAJO?(Z-K7Ls5Hj{*+3@#j@=H8d{{VkrJxl)pNo22; z^=}y8c(cGhD*I|go<@~(x62GOz6o=k%-`J@#yj<|sy;h^$t>|-{ebl^{UwsTnBu;% z6+5=IvwQ0%<$k)&$?Mg&O$=hjK~{@xOGMs|(DxFzM3ik5v`HkajQ;?FdPFk(GVrzC zx%)NjnjVa@Ttt!ivPFLcDJsD2B0ge1WXX(fjc1vmXr*s3c}|J&OU2(0JU!=sZP>+Y zWRY7)_gYw1OIt!Mh18ONYa|J7kRiR0?^#48^8(y|qYqZlz7Xo38PcsZO)tgTvCVjHNY#g_Y4@&AVKWHbo#bA?8o4f8kzu zi~ff_bN>A3{{Vmdt)u=G=ZL@PbJsub&X4~9%G%-jtv<^}-_BY$^GSWjn6ml{?5!J; z@|4n2eyv3(=zQtnzk)iKiF_~O2s|(1ojX?3?jx4P-pax$%*X8eirYwPq_R=}01ETOU-VQt>!0`MNB;ojZD;Gh2z5^r zX;*qCsp9Q&YYXTlwz!YV(pP*^p^in|nsU1CMk8{vu+CJ2gImIyrnzFQmdSbR`LnW( z7}1J^7c5rEY_9J9_S-*`P$MvtFa!ppfDZwZDulCW0(=3oEIdNs