From f8f28948403fb754e64ae1e8352229c65cdf14d1 Mon Sep 17 00:00:00 2001 From: Jonathan Bendes Date: Thu, 4 Sep 2025 12:55:05 -0400 Subject: [PATCH 1/2] Java revamp. Need to finish working out the kinks in java transports --- examples/env | 6 + examples/java/bitfield-pub.sh | 2 + examples/java/bitfield-sub.sh | 2 + examples/java/bitfield_pub.sh | 3 - examples/java/bitfield_sub.sh | 3 - .../java/example/apps/SimpleLogWriter.java | 44 + examples/java/pub.sh | 3 +- examples/java/simple-log-writer.sh | 2 + examples/java/sub.sh | 3 +- tools/java/zcm/logging/CsvReader.java | 298 ----- tools/java/zcm/logging/CsvReaderPlugin.java | 10 - tools/java/zcm/logging/JScrubber.java | 545 -------- tools/java/zcm/logging/JScrubberListener.java | 10 - tools/java/zcm/logging/LogDiagnostic.java | 47 - tools/java/zcm/logging/LogPlayer.java | 1118 ----------------- tools/java/zcm/logging/Logger.java | 56 - tools/java/zcm/logging/Transcoder.java | 316 ----- tools/java/zcm/logging/TranscoderPlugin.java | 18 - waf | 10 +- waftools/zcm-gen.py | 25 +- wscript | 1 + .../jni/zcm_zcm_ZCMGenericSerialTransport.c | 311 +++++ .../jni/zcm_zcm_ZCMGenericSerialTransport.h | 61 + zcm/java/jni/zcm_zcm_ZCMJNI.c | 24 +- zcm/java/jni/zcm_zcm_ZCMJNI.h | 8 + .../zcm/zcm}/BufferedRandomAccessFile.java | 11 +- .../zcm/logging => zcm/java/zcm/zcm}/Log.java | 7 +- zcm/java/zcm/zcm/SerialIO.java | 33 + zcm/java/zcm/zcm/ZCM.java | 17 +- .../zcm/zcm/ZCMGenericSerialTransport.java | 125 ++ zcm/java/zcm/zcm/ZCMJNI.java | 12 + zcm/java/zcm/zcm/ZCMTransport.java | 23 + 32 files changed, 702 insertions(+), 2452 deletions(-) create mode 100755 examples/java/bitfield-pub.sh create mode 100755 examples/java/bitfield-sub.sh delete mode 100755 examples/java/bitfield_pub.sh delete mode 100755 examples/java/bitfield_sub.sh create mode 100644 examples/java/example/apps/SimpleLogWriter.java create mode 100755 examples/java/simple-log-writer.sh delete mode 100644 tools/java/zcm/logging/CsvReader.java delete mode 100644 tools/java/zcm/logging/CsvReaderPlugin.java delete mode 100644 tools/java/zcm/logging/JScrubber.java delete mode 100644 tools/java/zcm/logging/JScrubberListener.java delete mode 100644 tools/java/zcm/logging/LogDiagnostic.java delete mode 100644 tools/java/zcm/logging/LogPlayer.java delete mode 100644 tools/java/zcm/logging/Logger.java delete mode 100644 tools/java/zcm/logging/Transcoder.java delete mode 100644 tools/java/zcm/logging/TranscoderPlugin.java create mode 100644 zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.c create mode 100644 zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.h rename {tools/java/zcm/util => zcm/java/zcm/zcm}/BufferedRandomAccessFile.java (98%) rename {tools/java/zcm/logging => zcm/java/zcm/zcm}/Log.java (97%) create mode 100644 zcm/java/zcm/zcm/SerialIO.java create mode 100644 zcm/java/zcm/zcm/ZCMGenericSerialTransport.java create mode 100644 zcm/java/zcm/zcm/ZCMTransport.java diff --git a/examples/env b/examples/env index 1b82411cb..c75bade15 100755 --- a/examples/env +++ b/examples/env @@ -40,6 +40,12 @@ if [ $ret -ne 0 ]; then export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$INSTALL_BASE/lib/ fi +echo $LD_LIBRARY_PATH | grep -q $INSTALL_BASE/lib64 +ret=$? +if [ $ret -ne 0 ]; then + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$INSTALL_BASE/lib64/ +fi + export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer export ASAN_OPTIONS=check_initialization_order=1 export CLASSPATH=$CLASSPATH:$INSTALL_BASE/share/java/zcm.jar diff --git a/examples/java/bitfield-pub.sh b/examples/java/bitfield-pub.sh new file mode 100755 index 000000000..0799acf8d --- /dev/null +++ b/examples/java/bitfield-pub.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -ea example.apps.BitfieldPub diff --git a/examples/java/bitfield-sub.sh b/examples/java/bitfield-sub.sh new file mode 100755 index 000000000..53249f798 --- /dev/null +++ b/examples/java/bitfield-sub.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -ea example.apps.BitfieldSub diff --git a/examples/java/bitfield_pub.sh b/examples/java/bitfield_pub.sh deleted file mode 100755 index 4c392b6f6..000000000 --- a/examples/java/bitfield_pub.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# warning, the classpaths set are relative so this script must be run from zcm/examples -CLASSPATH=$CLASSPATH:build/java/example.jar:build/types/examplezcmtypes.jar:/usr/local/share/java/zcm.jar java -ea example.apps.BitfieldPub diff --git a/examples/java/bitfield_sub.sh b/examples/java/bitfield_sub.sh deleted file mode 100755 index d0d80f16c..000000000 --- a/examples/java/bitfield_sub.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# warning, the classpaths set are relative so this script must be run from zcm/examples -CLASSPATH=$CLASSPATH:build/java/example.jar:build/types/examplezcmtypes.jar:/usr/local/share/java/zcm.jar java -ea example.apps.BitfieldSub diff --git a/examples/java/example/apps/SimpleLogWriter.java b/examples/java/example/apps/SimpleLogWriter.java new file mode 100644 index 000000000..84006b809 --- /dev/null +++ b/examples/java/example/apps/SimpleLogWriter.java @@ -0,0 +1,44 @@ +package example.apps; + +import java.io.*; +import zcm.zcm.*; + +/** + * A minimal example of writing events to a ZCM log file. + * This demonstrates the basic usage of the ZCM Log class for event logging. + */ +public class SimpleLogWriter { + + public static void main(String[] args) { + try { + // Create a log file for writing + Log log = new Log("events.log", "rw"); + + // Create some sample events + Log.Event event1 = new Log.Event(); + event1.eventNumber = 0; + event1.utime = System.currentTimeMillis() * 1000; // Convert to microseconds + event1.channel = "SENSOR_DATA"; + event1.data = "temperature:25.3".getBytes(); + + Log.Event event2 = new Log.Event(); + event2.eventNumber = 1; + event2.utime = System.currentTimeMillis() * 1000 + 1000000; // 1 second later + event2.channel = "STATUS"; + event2.data = "system:online".getBytes(); + + // Write events to log + log.write(event1); + log.write(event2); + + // Flush and close + log.flush(); + log.close(); + + System.out.println("Successfully wrote 2 events to events.log"); + + } catch (IOException e) { + System.err.println("Error writing to log: " + e.getMessage()); + } + } +} diff --git a/examples/java/pub.sh b/examples/java/pub.sh index 678c96a7f..d6d56c42b 100755 --- a/examples/java/pub.sh +++ b/examples/java/pub.sh @@ -1,3 +1,2 @@ #!/bin/bash -# warning, the classpaths set are relative so this script must be run from zcm/examples -CLASSPATH=$CLASSPATH:build/java/example.jar:build/types/examplezcmtypes.jar:/usr/local/share/java/zcm.jar java -ea example.apps.Pub +java -ea example.apps.Pub diff --git a/examples/java/simple-log-writer.sh b/examples/java/simple-log-writer.sh new file mode 100755 index 000000000..a7bb20947 --- /dev/null +++ b/examples/java/simple-log-writer.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -ea example.apps.SimpleLogWriter diff --git a/examples/java/sub.sh b/examples/java/sub.sh index 954008493..cefc3c7c0 100755 --- a/examples/java/sub.sh +++ b/examples/java/sub.sh @@ -1,3 +1,2 @@ #!/bin/bash -# warning, the classpaths set are relative so this script must be run from zcm/examples -CLASSPATH=$CLASSPATH:build/java/example.jar:build/types/examplezcmtypes.jar:/usr/local/share/java/zcm.jar java -ea example.apps.Sub +java -ea example.apps.Sub diff --git a/tools/java/zcm/logging/CsvReader.java b/tools/java/zcm/logging/CsvReader.java deleted file mode 100644 index 03b187f8b..000000000 --- a/tools/java/zcm/logging/CsvReader.java +++ /dev/null @@ -1,298 +0,0 @@ -package zcm.logging; - -import java.io.*; -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.ArrayList; - -import zcm.zcm.*; -import zcm.util.*; - -public class CsvReader -{ - private BufferedReader input; - private long inputSize = 0; - private Log outputLog = null; - private ZCM outputZcm = null; - private CsvReaderPlugin plugin; - - private int numEventsRead = 0; - - private boolean verbose = false; - private boolean done = false; - - public CsvReader(Log outputLog, String zcm_url, - BufferedReader input, long inputSize, - Constructor pluginCtor, boolean verbose) - { - this.input = input; - this.inputSize = inputSize; - this.outputLog = outputLog; - if (pluginCtor != null) { - try { - plugin = (CsvReaderPlugin) pluginCtor.newInstance(); - System.out.println("Found plugin: " + plugin.getClass().getName()); - } catch (Exception ex) { - System.out.println("ex: " + ex); - ex.printStackTrace(); - System.exit(1); - } - } - this.verbose = verbose; - - if (this.verbose) System.out.println("Debugging information turned on"); - - if (outputLog == null) { - try { - outputZcm = new ZCM(zcm_url); - System.out.println("Outputting messages over zcm"); - } catch (IOException e) { - System.err.println("Unable to parse zcm url output"); - System.exit(1); - } - } else { - System.out.println("Generating logfile from csv"); - } - } - - private void verbosePrintln(String str) - { - if(verbose) System.out.println(str); - } - - private void verbosePrint(String str) - { - if(verbose) System.out.print(str); - } - - public void run() - { - Runtime.getRuntime().addShutdownHook(new Thread() { - public void run() { - System.out.print("\b\b "); - try { - if (outputLog != null) - outputLog.close(); - } catch (IOException e) { - System.err.println("Unable to close output file"); - } - System.out.println("Read " + numEventsRead + " events"); - System.out.println("Cleaning up and quitting"); - } - }); - long lastPrintTime = 0; - long bytesRead = 0; - while (!done) { - try { - String line = input.readLine(); - if (line == null) { - System.out.println("\rProgress: 100% "); - break; - } - ArrayList events = plugin.readZcmType(line); - if (events == null || events.size() == 0) continue; - numEventsRead += events.size(); - - if (outputLog != null) { - for (Log.Event e : events) - outputLog.write(e); - } - if (outputZcm != null) { - for (Log.Event e : events) - outputZcm.publish(e.channel, e.data, 0, e.data.length); - } - bytesRead += line.length(); - double percent = (double) bytesRead / (double) this.inputSize * 100; - long now = System.currentTimeMillis() * 1000; - if (lastPrintTime + 5e5 < now) { - lastPrintTime = now; - System.out.print("\rProgress: " + String.format("%.2f", percent) + "%"); - System.out.flush(); - } - } catch (IOException e) { - System.err.println("Unable to read line from csv"); - } - } - } - - public static class PluginClassVisitor implements ClassDiscoverer.ClassVisitor - { - public HashMap plugins = new HashMap(); - - public PluginClassVisitor() - { - ClassDiscoverer.findClasses(this); - } - - public void classFound(String jar, Class cls) - { - Class c = cls; - if (!c.equals(CsvReaderPlugin.class) && CsvReaderPlugin.class.isAssignableFrom(c)) { - try { - Constructor ctr = c.getConstructor(); - CsvReaderPlugin plugin = (CsvReaderPlugin) ctr.newInstance(); - plugins.put(plugin.getClass().getName(), ctr); - } catch (Exception ex) { - System.out.println("ex: " + ex); - ex.printStackTrace(); - } - } - } - - public void print() - { - if (plugins.keySet().size() == 0) { - System.err.println("No plugins found"); - return; - } - - for (String name : plugins.keySet()) - System.out.println("Found " + name); - } - } - - public static void usage() - { - System.out.println("usage: zcm-csv-reader [options]"); - System.out.println(""); - System.out.println(" Reads data from a csv file and converts the data into"); - System.out.println(" ZCM messages. ZCM messages are then either broadcast"); - System.out.println(" over a zcm transport or written to a logfile."); - System.out.println(""); - System.out.println("Options:"); - System.out.println(""); - System.out.println(" -c, --csv= Input csv file."); - System.err.println(" -u, --zcm-url=URL Output traffic over the specified ZCM URL"); - System.out.println(" Please provide only one of -u/-l"); - System.out.println(" -o, --output= Output traffic to log file"); - System.out.println(" -p, --plugin= Use csv reader plugin"); - System.out.println(" --list-plugins List available csv reader plugins"); - System.out.println(" -h, --help Display this help message"); - } - - public static void main(String args[]) - { - String zcm_url = null; - String csvfile = null; - String logfile = null; - String pluginName = null; - - boolean list_plugins = false; - boolean verbose = false; - - int i; - for (i = 0; i < args.length; ++i) { - String toks[] = args[i].split("="); - String arg = toks[0]; - if (arg.equals("-u") || arg.equals("--zcm-url")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - zcm_url = param; - } else if (arg.equals("-c") || arg.equals("--csv")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - csvfile = param; - } else if (arg.equals("-o") || arg.equals("--output")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - logfile = param; - } else if (arg.equals("-p") || arg.equals("--plugin")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - pluginName = param; - } else if (arg.equals("--list-plugins")) { - list_plugins = true; - } else if (arg.equals("-v") || arg.equals("--verbose")) { - verbose = true; - } else if (arg.equals("-h") || arg.equals("--help")) { - usage(); - System.exit(0); - } else { - System.err.println("Unrecognized command line option: '" + arg + "'"); - usage(); - System.exit(1); - } - } - - if (list_plugins) { - System.out.println("Searching path for plugins"); - PluginClassVisitor pcv = new PluginClassVisitor(); - pcv.print(); - System.exit(0); - } - - // Check input getopt - if (csvfile == null) { - System.err.println("Please specify an input csv file"); - usage(); - System.exit(1); - } - - // Check output getopt - if (zcm_url == null && logfile == null) { - System.err.println("Please specify at least one source of zcm data"); - usage(); - System.exit(1); - } - - Constructor pluginCtor = null; - if (pluginName != null) { - System.out.println("Searching path for plugins"); - PluginClassVisitor pcv = new PluginClassVisitor(); - pluginCtor = pcv.plugins.get(pluginName); - if (pluginCtor == null) { - System.err.println("Unable to find specified plugin"); - System.exit(1); - } - } - - // Setup input file - BufferedReader input = null; - long fileSize = 0; - try { - File inputFile = new File(csvfile); - fileSize = inputFile.length(); - input = new BufferedReader(new FileReader(inputFile)); - } catch(Exception e) { - System.err.println("Unable to open " + csvfile); - System.exit(1); - } - - // Setup output file. - Log output = null; - if (logfile != null) { - try { - output = new Log(logfile, "rw"); - } catch(Exception e) { - System.err.println("Unable to open " + logfile); - System.exit(1); - } - } - - new CsvReader(output, zcm_url, input, fileSize, pluginCtor, verbose).run(); - } -} diff --git a/tools/java/zcm/logging/CsvReaderPlugin.java b/tools/java/zcm/logging/CsvReaderPlugin.java deleted file mode 100644 index 65c3fd44b..000000000 --- a/tools/java/zcm/logging/CsvReaderPlugin.java +++ /dev/null @@ -1,10 +0,0 @@ -package zcm.logging; - -import java.util.ArrayList; - -import zcm.logging.*; - -public abstract class CsvReaderPlugin -{ - public abstract ArrayList readZcmType(String line); -} diff --git a/tools/java/zcm/logging/JScrubber.java b/tools/java/zcm/logging/JScrubber.java deleted file mode 100644 index 6e306160b..000000000 --- a/tools/java/zcm/logging/JScrubber.java +++ /dev/null @@ -1,545 +0,0 @@ -package zcm.logging; - -import java.awt.*; -import java.awt.event.*; -import java.awt.geom.*; -import javax.swing.*; -import javax.swing.event.*; - -import java.util.*; - -public class JScrubber extends JComponent -{ - static final int BARHEIGHT = 5; - static final int KNOBSIZE = 10; - static final int MARGIN = 10; - static final int MIN_HEIGHT = KNOBSIZE + 2*MARGIN + BARHEIGHT*5; - - static final int BOOKMARK_HEIGHT = 15, BOOKMARK_WIDTH = 6; - - static final int CLICK_CLOSENESS = 5; - - static final int REPEAT_DOT_SIZE = 4; - - double position = 0.5; - - double zoomfrac = 0.1; - double zoom0, zoom1; // positions (0,1) representing the left and right end of the zoom scrubber - int cy, cy2; - - int lastDrawX = -1, lastDrawX2 = -1; // used to eliminate extra redraws - - ArrayList listeners = new ArrayList(); - - boolean inhibitGeometryChanges = false; - - JPopupMenu popupMenu = new JPopupMenu(); - double popupPosition; - - boolean enabled = true; - - public static final int BOOKMARK_PLAIN = 0, BOOKMARK_LREPEAT = 1, BOOKMARK_RREPEAT = 2; - class Bookmark - { - double position; - int type; - - Bookmark(double p, int type) - { - this.position = p; - this.type = type; - } - } - - ArrayList bookmarks = new ArrayList(); - - public JScrubber() - { - MyMouseAdapter ma = new MyMouseAdapter(); - addMouseListener(ma); - addMouseMotionListener(ma); - - popupMenu.add(new PopupAction("Bookmark", BOOKMARK_PLAIN)); - popupMenu.add(new PopupAction("Right repeat :]", BOOKMARK_RREPEAT)); - popupMenu.add(new PopupAction("Left repeat [:", BOOKMARK_LREPEAT)); - popupMenu.addSeparator(); - - int zooms[] = new int[] {10, 20, 50, 100, 200, 500, 1000}; - ButtonGroup group = new ButtonGroup(); - for (int i = 0; i < zooms.length; i++) { - JRadioButtonMenuItem jmi = new JRadioButtonMenuItem("Zoom "+zooms[i]+"x"); - jmi.addActionListener(new ZoomAction("foo", zooms[i])); - group.add(jmi); - popupMenu.add(jmi); - - if (i==3) { - jmi.setSelected(true); - zoomfrac = 1.0 / zooms[i]; - } - } - - popupMenu.addSeparator(); - popupMenu.add(new ExportAction()); - } - - @Override - public void setEnabled(boolean enabled) - { - super.setEnabled(enabled); - this.enabled = enabled; - } - - public void clearBookmarks() - { - bookmarks = new ArrayList(); - repaint(); - } - - public void addBookmark(int type, double position) - { - bookmarks.add(new Bookmark(position, type)); - repaint(); - } - - public ArrayList getBookmarks() - { - return bookmarks; - } - - class ExportAction extends AbstractAction - { - public ExportAction() - { - super("Export log snippet..."); - } - - public void actionPerformed(ActionEvent e) - { - Bookmark b0 = new Bookmark(0, BOOKMARK_PLAIN); - Bookmark b1 = new Bookmark(1, BOOKMARK_PLAIN); - - if (bookmarks.size() == 0) { - System.out.println("didn't find bookmark region"); - return; - } else if (bookmarks.size() == 1){ - //if there is only one bookmark, use from beginning/end to it - Bookmark b = bookmarks.get(0); - if (position > b.position) - b0 = b; - else - b1 = b; - } else if (bookmarks.size() == 2) { - // if there are only two bookmarks, just use those. - b0 = bookmarks.get(0); - b1 = bookmarks.get(1); - if (b0.position>b1.position){ //b0 should be before b1 - Bookmark swp = b0; - b0 = b1; - b1 = b0; - } - } else { - // find previous and next book marks. - for (Bookmark b : bookmarks) { - if (b.position < position && (b0 == null || b0.position < b.position)) - b0 = b; - if (b.position > position && (b1 == null || b1.position > b.position)) - b1 = b; - } - } - - - for (JScrubberListener jsl : listeners) - jsl.scrubberExportRegion(JScrubber.this, b0.position, b1.position); - - } - } - - class ZoomAction extends AbstractAction - { - int zoom; - - public ZoomAction(String name, int zoom) - { - super(name); - this.zoom = zoom; - } - - public void actionPerformed(ActionEvent e) - { - setZoomFraction(1.0/zoom); - } - } - - class PopupAction extends AbstractAction - { - int op; - - public PopupAction(String name, int op) - { - super(name); - - this.op = op; - } - - public void actionPerformed(ActionEvent e) - { - bookmarks.add(new Bookmark(popupPosition, op)); - repaint(); - } - } - - public double getZoomFraction() - { - return zoomfrac; - } - - public void setZoomFraction(double f) - { - zoomfrac = f; - updateGeometry(); - repaint(); - } - - public void addScrubberListener(JScrubberListener l) - { - listeners.add(l); - } - - public Dimension getMinimumSize() - { - return new Dimension(100, MIN_HEIGHT); - } - - public Dimension getPreferredSize() - { - Dimension d = super.getPreferredSize(); - return new Dimension((int) d.getWidth(), (int) Math.max(d.getHeight(), MIN_HEIGHT)); - } - - // convert a position to an X coordinate for the main scrubber - int getX(double v) - { - return (int) (MARGIN + v*(getWidth()-MARGIN*2)); - } - - // convert a position to an X coordinate for the zoom scrubber - int getX2(double v) - { - return getX( (v-zoom0)/(zoom1-zoom0) ); - } - - // convert an X coordinate to a position for the main scrubber - double getPosition(int x) - { - double pos = ((double) x - MARGIN)/(getWidth() - 2*MARGIN); - if (pos < 0) - pos = 0; - if (pos > 1) - pos = 1; - return pos; - } - - // convert an X coordinate to a position for the main scrubber - double getPosition2(int x) - { - double pos = ((double) x - MARGIN)/(getWidth() - 2*MARGIN); - if (pos < 0) - return zoom0; - if (pos > 1) - return zoom1; - - double pos2 = pos * (zoom1-zoom0) + zoom0; - return pos2; - } - - int getRow(int x, int y) - { - return y > ((cy + cy2)/2) ? 1 : 0; - } - - double getPosition(int x, int y) - { - double position; - - if (getRow(x, y) == 0) - position = getPosition(x); - else - position = getPosition2(x); - - return position; - } - - void updateGeometry() - { - if (inhibitGeometryChanges) - return; - - if (position < zoomfrac/2) { - zoom0 = 0; - zoom1 = zoomfrac; - } else if (position > 1-zoomfrac/2) { - zoom0 = 1-zoomfrac; - zoom1 = 1; - } else { - zoom0 = position-zoomfrac/2; - zoom1 = position+zoomfrac/2; - } - - cy = getHeight()/3; - cy2 = cy + BARHEIGHT*4; - } - - void drawBookmark(Graphics g, Bookmark b, int x, int cy) - { - g.setColor(new Color(0, 200, 0)); - g.fillRect(x-BOOKMARK_WIDTH/2, cy - BOOKMARK_HEIGHT/2, BOOKMARK_WIDTH, BOOKMARK_HEIGHT); - g.setColor(new Color(0, 100, 0)); - g.drawRect(x-BOOKMARK_WIDTH/2, cy - BOOKMARK_HEIGHT/2, BOOKMARK_WIDTH, BOOKMARK_HEIGHT); - - if (b.type == BOOKMARK_LREPEAT) { - g.setColor(new Color(0, 50, 0)); - g.fillRect(x + BOOKMARK_WIDTH/2, cy - BOOKMARK_HEIGHT/2, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE); - g.fillRect(x + BOOKMARK_WIDTH/2, cy + BOOKMARK_HEIGHT/2 - REPEAT_DOT_SIZE, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE); - } - - if (b.type == BOOKMARK_RREPEAT) { - g.setColor(new Color(0, 50, 0)); - g.fillRect(x - BOOKMARK_WIDTH/2 - REPEAT_DOT_SIZE, cy - BOOKMARK_HEIGHT/2, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE); - g.fillRect(x - BOOKMARK_WIDTH/2 - REPEAT_DOT_SIZE, cy + BOOKMARK_HEIGHT/2 - REPEAT_DOT_SIZE, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE); - } - } - - public void paint(Graphics g) - { - Graphics2D g2d = (Graphics2D) g; - int width = getWidth(), height = getHeight(); - int margin = 4; - int barheight = 10; - - g.setColor(getBackground()); - g.fillRect(0,0, width, height); - - double position2 = (position - zoom0)/zoomfrac; - - // draw the trapezoid between the two bars - g.setColor(Color.lightGray); - GeneralPath gp = new GeneralPath(); - gp.moveTo(getX(zoom0), cy + BARHEIGHT/2+1); - gp.lineTo(getX(zoom1), cy + BARHEIGHT/2+1); - gp.lineTo(getX(1.0), cy2 - BARHEIGHT/2); - gp.lineTo(getX(0.0), cy2 - BARHEIGHT/2); - gp.closePath(); - // g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.fill(gp); - - // draw the bar - g.setColor(new Color(150, 150, 255)); - g.fillRect(MARGIN, cy - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT); - g.setColor(Color.blue); - g.drawRect(MARGIN, cy - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT); - - g.setColor(new Color(150, 255, 255)); - g.fillRect(MARGIN, cy2 - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT); - g.setColor(Color.blue); - g.drawRect(MARGIN, cy2 - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT); - - g.setColor(Color.darkGray); - g.fillRect(getX(zoom0), cy - BARHEIGHT*1/3, getX(zoom1)-getX(zoom0), BARHEIGHT); - - for (Bookmark b : bookmarks) - { - drawBookmark(g, b, getX(b.position), cy); - if (b.position >= zoom0 && b.position <= zoom1) - drawBookmark(g, b, getX2(b.position), cy2); - } - - // draw the knob - g.setColor(Color.yellow); - g.fillOval(getX(position)-KNOBSIZE/2, cy - KNOBSIZE/2, KNOBSIZE, KNOBSIZE); - g.setColor(Color.black); - g.drawOval(getX(position)-KNOBSIZE/2, cy - KNOBSIZE/2, KNOBSIZE, KNOBSIZE); - - g.setColor(Color.yellow); - g.fillOval(getX2(position)-KNOBSIZE/2, cy2 - KNOBSIZE/2, KNOBSIZE, KNOBSIZE); - g.setColor(Color.black); - g.drawOval(getX2(position)-KNOBSIZE/2, cy2 - KNOBSIZE/2, KNOBSIZE, KNOBSIZE); - - lastDrawX = getX(position); - lastDrawX2 = getX2(position); - } - - void userSet(double newpos) - { - position = newpos; - - for (JScrubberListener l : listeners) - l.scrubberMovedByUser(this, position); - - updateGeometry(); - - repaint(); - } - - synchronized public void set(double pos) - { - double oldpos = this.position; - - this.position = pos; - - updateGeometry(); - // now we've updated geometry, maybe we don't need to draw. - if (Math.abs(getX(position) - lastDrawX) > 1 || Math.abs(getX2(position) - lastDrawX2) > 1) - repaint(); - - for (Bookmark b : bookmarks) - { - if (b.type == BOOKMARK_RREPEAT && b.position > oldpos && b.position <= pos) - { - Bookmark lrepeat = null; - // find most recent LREPEAT - for (Bookmark bl : bookmarks) - { - if (bl.position < b.position && bl.type == BOOKMARK_LREPEAT && (lrepeat == null || bl.position > lrepeat.position)) - lrepeat = bl; - } - - double lposition = (lrepeat == null) ? 0 : lrepeat.position; - for (JScrubberListener l : listeners) - { - l.scrubberPassedRepeat(this, b.position, lposition); - } - } - } - } - - class MyMouseAdapter extends MouseInputAdapter - { - Bookmark trackbookmark; - - Bookmark findBookmark(double position, double tolerance) - { - double minerr = tolerance; - Bookmark best = null; - - for (Bookmark b : bookmarks) - { - double thiserr = Math.abs(position - b.position); - if (thiserr < minerr) - { - best = b; - minerr = thiserr; - } - } - - return best; - } - - public void mousePressed(MouseEvent e) - { - if (!enabled) return; - - double position = getPosition(e.getX(), e.getY()); - double tolerance = getPosition(e.getX()+CLICK_CLOSENESS, e.getY()) - position; - - if (e.getButton()==3) - { - trackbookmark = findBookmark(position, tolerance); - } - } - - public void mouseReleased(MouseEvent e) - { - if (!enabled) return; - - if (trackbookmark != null) - { - if (trackbookmark.position == 0 || trackbookmark.position == 1) - { - bookmarks.remove(trackbookmark); - repaint(); - } - - trackbookmark = null; - } - - inhibitGeometryChanges = false; - } - - public void mouseClicked(MouseEvent e) - { - if (!enabled) return; - - int mods=e.getModifiersEx(); - boolean shift = (mods&MouseEvent.SHIFT_DOWN_MASK)>0; - boolean ctrl = (mods&MouseEvent.CTRL_DOWN_MASK)>0; - boolean alt = shift & ctrl; - ctrl = ctrl & (!alt); - shift = shift & (!alt); - boolean nomods = !(shift | ctrl | alt); - double tolerance = getPosition(e.getX()+CLICK_CLOSENESS, e.getY()) - position; - - double position = getPosition(e.getX(), e.getY()); - - if (e.getButton()==1) - { - Bookmark nearest = findBookmark(position, tolerance); - if (nearest == null) - userSet(position); - else - userSet(nearest.position); - } - - if (e.getButton()==3) - { - popupPosition = position; - popupMenu.show(JScrubber.this, e.getX(), e.getY()); - /* - int err = Math.abs(e.getX() - getX(position)); - - // if they clicked near the current cursor, create - // a bookmark exactly where the cursor is - int type = BOOKMARK_PLAIN; - if (shift) - type = BOOKMARK_LREPEAT; - if (ctrl) - type = BOOKMARK_RREPEAT; - - if (err < CLICK_CLOSENESS) - bookmarks.add(new Bookmark(position, type)); - else - bookmarks.add(new Bookmark(position, type)); - - repaint(); - */ - } - } - - public void mouseDragged(MouseEvent e) - { - if (!enabled) return; - - double position = getPosition(e.getX(), e.getY()); - int row = getRow(e.getX(), e.getY()); - - if (row == 1) - inhibitGeometryChanges = true; - else - inhibitGeometryChanges = false; - - if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) - { - userSet(position); - } - - if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) - { - if (trackbookmark != null) - { - trackbookmark.position = position; - repaint(); - } - } - } - - } -} diff --git a/tools/java/zcm/logging/JScrubberListener.java b/tools/java/zcm/logging/JScrubberListener.java deleted file mode 100644 index 75b7f5c09..000000000 --- a/tools/java/zcm/logging/JScrubberListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package zcm.logging; - -public interface JScrubberListener -{ - public void scrubberMovedByUser(JScrubber js, double x); - - public void scrubberPassedRepeat(JScrubber js, double from_pos, double to_pos); - - public void scrubberExportRegion(JScrubber js, double p0, double p1); -} diff --git a/tools/java/zcm/logging/LogDiagnostic.java b/tools/java/zcm/logging/LogDiagnostic.java deleted file mode 100644 index 615d23749..000000000 --- a/tools/java/zcm/logging/LogDiagnostic.java +++ /dev/null @@ -1,47 +0,0 @@ -package zcm.logging; - -import java.awt.*; -import java.awt.event.*; -import java.io.*; -import java.util.*; -import java.net.*; -import java.util.concurrent.*; - -import javax.swing.*; -import javax.swing.event.*; -import javax.swing.table.*; - -import zcm.zcm.*; -import zcm.util.*; - -public class LogDiagnostic -{ - public static void main(String args[]) - { - try { - main_ex(args); - } catch (IOException ex) { - System.out.println("ex: "+ex); - } - } - - public static void main_ex(String args[]) throws IOException - { - Log log = new Log(args[0], "r"); - - long last_utime = 0; - - while (true) { - Log.Event e = log.readNext(); - - long dutime = e.utime - last_utime; - if (dutime < 0 && last_utime != 0) - System.out.printf("%15d Negative utime (%10d)\n", e.utime, dutime); - if (dutime > 1000000) - System.out.printf("%15d Large utime (%10d)\n", e.utime, dutime); - - last_utime = e.utime; - } - - } -} diff --git a/tools/java/zcm/logging/LogPlayer.java b/tools/java/zcm/logging/LogPlayer.java deleted file mode 100644 index dfae2df9e..000000000 --- a/tools/java/zcm/logging/LogPlayer.java +++ /dev/null @@ -1,1118 +0,0 @@ -package zcm.logging; - -import java.awt.*; -import java.awt.event.*; -import java.awt.image.*; -import java.awt.geom.*; -import java.io.*; -import java.net.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.regex.*; - -import javax.swing.*; -import javax.swing.event.*; -import javax.swing.table.*; - -import zcm.zcm.*; -import zcm.util.*; - -import static java.awt.GridBagConstraints.*; - -/** A GUI implementation of a log player allowing seeking. **/ -public class LogPlayer extends JComponent -{ - Log log; - JButton playButton = new JButton("Play "); - JButton stepButton = new JButton("Step"); - JButton fasterButton; - // JButton fasterButton = new JButton(">>"); - JButton slowerButton; // = new JButton("<<"); - JLabel speedLabel = new JLabel("1.0", JLabel.CENTER); - double speed = 1.0; - - static final int POS_MAX = 10000; - - JLabel posLabel = new JLabel("Event 0"); - JLabel timeLabel = new JLabel("Time 0.0s"); - JLabel actualSpeedLabel = new JLabel("1.0x"); - - JLabel logName = new JLabel("---"); - - PlayerThread player = null; - - ZCM zcm; - - /** The time of the first event in the current log **/ - long timeOffset = 0; - - JFileChooser jfc = new JFileChooser(); - - String currentLogPath; - - double total_seconds; // an estimate of how many seconds there are in the file - - BlockingQueue events = new LinkedBlockingQueue(); - - Object sync = new Object(); - - interface QueuedEvent - { - public void execute(LogPlayer lp); - } - - class QueueThread extends Thread - { - public void run() - { - while (true) { - try { - QueuedEvent qe = events.take(); - qe.execute(LogPlayer.this); - } catch (InterruptedException ex) { - } - } - } - } - - /** We have events coming from all over the place: the UI, UDP - events, callbacks from the scrubbers. To keep things sanely - thread-safe, all of these things simply queue events which are - processed in-order. doStop, doPlay, doStep, do(Anything) can - only be called from the queue thread. - **/ - class PlayPauseEvent implements QueuedEvent - { - boolean toggle = false; - boolean playstate; - - public PlayPauseEvent() - { - this.toggle = true; - } - - public PlayPauseEvent(boolean playstate) - { - this.playstate = playstate; - } - - public void execute(LogPlayer lp) - { - if (toggle) - { - if (player!=null) - doStop(); - else - doPlay(); - } - else - { - if (playstate) - doPlay(); - else - doStop(); - } - } - } - - // seek, preserving the current play/pause state - class SeekEvent implements QueuedEvent - { - double pos; - - public SeekEvent(double pos) - { - this.pos = pos; - } - - public void execute(LogPlayer lp) - { - boolean player_was_running = (player != null); - - if (player_was_running) - doStop(); - - doSeek(pos); - - if (player_was_running) - doPlay(); - } - } - - class StepEvent implements QueuedEvent - { - public void execute(LogPlayer lp) - { - doStep(); - } - } - - class Filter implements Comparable - { - String inchannel; - String outchannel; - boolean enabled = true; - - public int compareTo(Filter f) - { - return inchannel.compareTo(f.inchannel); - } - } - - Pattern filteredPattern; - boolean invertFilteredPattern; - FilterTableModel filterTableModel = new FilterTableModel(); - ArrayList filters = new ArrayList(); - // JTable calls upon filterTableModel which calls upon filters... - // which needs to exist before that! - JTable filterTable = new JTable(filterTableModel); - HashMap filterMap = new HashMap(); - - JTextField inchannel = new JTextField(); - JTextField outchannel = new JTextField(); - - JScrubber js = new JScrubber(); - - boolean show_absolute_time = false; - - JTextField stepChannelField = new JTextField(""); - - // faster/slower would be better as semi-log. - static final double slowerSpeed(double v) - { - return v/2; - } - - static final double fasterSpeed(double v) - { - return v*2; - } - - void setSpeed(double v) - { - v = Math.max(1.0/1024, v); // minimum supported speed (0.000977x) - speedLabel.setText(String.format("%.3f", v)); - speed = v; - } - - void setChannelFilter(String channelFilterRegex) - { - filteredPattern = Pattern.compile(channelFilterRegex); - } - void invertChannelFilter(){ - invertFilteredPattern = true; - } - - public LogPlayer(String zcmurl) throws IOException - { - setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - - filteredPattern = null; - invertFilteredPattern = false; - - Insets insets = new Insets(0,0,0,0); - int row = 0; - - logName.setText("Double click to load log"); - logName.setFont(new Font("SansSerif", Font.PLAIN, 10)); - - timeLabel.setFont(new Font("SansSerif", Font.PLAIN, 10)); - posLabel.setFont(new Font("SansSerif", Font.PLAIN, 10)); - actualSpeedLabel.setFont(new Font("SansSerif", Font.PLAIN, 10)); - - fasterButton = new JButton(new ImageIcon(makeArrowImage(Color.blue, getBackground(), false))); - fasterButton.setRolloverIcon(new ImageIcon(makeArrowImage(Color.magenta, getBackground(), false))); - fasterButton.setPressedIcon(new ImageIcon(makeArrowImage(Color.red, getBackground(), false))); - fasterButton.setBorderPainted(false); - fasterButton.setContentAreaFilled(false); - // Borders keep appearing when the buttons are pressed. Not sure why. - //fasterButton.setBorder(null); //new javax.swing.border.EmptyBorder(0,0,0,0)); - - slowerButton = new JButton(new ImageIcon(makeArrowImage(Color.blue, getBackground(), true))); - slowerButton.setRolloverIcon(new ImageIcon(makeArrowImage(Color.magenta, getBackground(), true))); - slowerButton.setPressedIcon(new ImageIcon(makeArrowImage(Color.red, getBackground(), true))); - slowerButton.setBorderPainted(false); - slowerButton.setContentAreaFilled(false); - - Font buttonFont = new Font("SansSerif", Font.PLAIN, 10); - fasterButton.setFont(buttonFont); - slowerButton.setFont(buttonFont); - playButton.setFont(buttonFont); - stepButton.setFont(buttonFont); - - JPanel p = new JPanel(); - p.setLayout(new GridLayout(1,3,0,0)); - p.add(slowerButton); - p.add(speedLabel); - p.add(fasterButton); - // x y w h fillx filly anchor fill insets, ix, iy - add(logName, - new GridBagConstraints(0, row, 1, 1, 0.0, 0.0, WEST, NONE, insets, 0, 0)); - - add(playButton, - new GridBagConstraints(1, row, 1, 1, 0.0, 0.0, CENTER, NONE, insets, 0, 0)); - add(stepButton, - new GridBagConstraints(2, row, 1, 1, 0.0, 0.0, CENTER, NONE, insets, 0, 0)); - - add(p, - new GridBagConstraints(3, row, REMAINDER, 1, 0.0, 0.0, EAST, NONE, insets, 0, 0)); - row++; - - add(js, - new GridBagConstraints(0, row, REMAINDER, 1, 1.0, 0.0, CENTER, HORIZONTAL, new Insets(0, 5, 0, 5), 0, 0)); - row++; - - add(timeLabel, - new GridBagConstraints(0, row, 1, 1, 0.0, 0.0, WEST, NONE, new Insets(0, 10, 0, 0), 0, 0)); - add(actualSpeedLabel, - new GridBagConstraints(1, row, 1, 1, 0.0, 0.0, WEST, NONE, new Insets(0, 10, 0, 0), 0, 0)); - add(posLabel, - new GridBagConstraints(3, row, 1, 1, 0.0, 0.0, EAST, NONE, new Insets(0,0, 0, 10), 0, 0)); - row++; - - add(new JScrollPane(filterTable), - new GridBagConstraints(0, row, REMAINDER, 1, 1.0, 1.0, CENTER, BOTH, new Insets(0,0, 0, 0), 0, 0)); - row++; - - /// spacers - - add(Box.createHorizontalStrut(90), - new GridBagConstraints(0, row, 1, 1, 0.0, 0.0, WEST, NONE, insets, 0, 0)); - add(Box.createHorizontalStrut(100), - new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, WEST, NONE, insets, 0, 0)); - - /////////////////////////// - row++; - JPanel stepPanel = new JPanel(new BorderLayout()); - stepPanel.add(new JLabel("Channel Prefix: "), BorderLayout.WEST); - stepPanel.add(stepChannelField, BorderLayout.CENTER); - - JButton toggleAllButton = new JButton("Toggle Selected"); - toggleAllButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - int[] rowIndices = filterTable.getSelectedRows(); - for (int i = 0; i < rowIndices.length; ++i) { - Filter f = filters.get(rowIndices[i]); - f.enabled = !f.enabled; - } - filterTableModel.fireTableDataChanged(); - for (int i = 0; i < rowIndices.length; ++i) { - filterTable.addRowSelectionInterval(rowIndices[i], - rowIndices[i]); - } - }}); - add(toggleAllButton, - new GridBagConstraints(0, row, 2, 1, 0.0, 0.0, CENTER, NONE, - insets, 0, 0)); - - add(stepPanel, - new GridBagConstraints(2, row, REMAINDER, 1, 1.0, 0.0, CENTER, HORIZONTAL, new Insets(0, 5, 0, 5), 0, 0)); - // position.addChangeListener(new MyChangeListener()); - setPlaying(false); - - fasterButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - setSpeed(fasterSpeed(speed)); - }}); - slowerButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - setSpeed(slowerSpeed(speed)); - }}); - - playButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - events.offer(new PlayPauseEvent()); - }}); - - stepButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - events.offer(new StepEvent()); - } - }); - - zcm = new ZCM(zcmurl); - - logName.addMouseListener(new MouseAdapter() { - public void mouseClicked(MouseEvent e) { - if (e.getClickCount()==2) - openDialog(); - } - }); - timeLabel.addMouseListener(new MouseAdapter() { - public void mouseClicked(MouseEvent e) { - show_absolute_time = ! show_absolute_time; - } - }); - - js.set(0); - js.addScrubberListener(new MyScrubberListener()); - - filterTable.getColumnModel().getColumn(2).setMaxWidth(50); - - playButton.setEnabled(false); - stepButton.setEnabled(false); - fasterButton.setEnabled(false); - slowerButton.setEnabled(false); - js.setEnabled(false); - - new UDPThread().start(); - new QueueThread().start(); - } - - class MyScrubberListener implements JScrubberListener - { - public void scrubberMovedByUser(JScrubber js, double x) - { - events.offer(new SeekEvent(x)); - } - - public void scrubberPassedRepeat(JScrubber js, double from_pos, double to_pos) - { - events.offer(new SeekEvent(to_pos)); - } - - public void scrubberExportRegion(JScrubber js, double p0, double p1) - { - System.out.printf("Export %15f %15f\n", p0, p1); - - String outpath = getOutputFileFromDialog(); - if (outpath == null) - return; - - System.out.println("Exporting to "+outpath); - try { - Log inlog = new Log(log.getPath(), "r"); - Log outlog = new Log(outpath, "rw"); - - inlog.seekPositionFraction(p0); - while (inlog.getPositionFraction() < p1) { - Log.Event e = inlog.readNext(); - Filter f = filterMap.get(e.channel); - if (f != null && f.enabled) - outlog.write(e); - } - inlog.close(); - outlog.close(); - System.out.printf("Done!\n"); - - } catch (IOException ex) { - System.out.println("Exception: "+ex); - } - } - } - - // remote control via UDP packets - class UDPThread extends Thread - { - public void run() - { - DatagramSocket sock; - DatagramPacket packet = new DatagramPacket(new byte[1024], 1024); - - try { - sock = new DatagramSocket(53261, Inet4Address.getByName("127.0.0.1")); - } catch (SocketException ex) { - System.out.println("Exception: "+ex); - return; - } catch (UnknownHostException ex) { - System.out.println("Exception: "+ex); - return; - } - - while (true) - { - try { - sock.receive(packet); - String cmd = new String(packet.getData(), 0, packet.getLength()); - cmd = cmd.trim(); - - if (cmd.equals("PLAYPAUSETOGGLE")) { - events.offer(new PlayPauseEvent()); - } else if (cmd.equals("PLAY")) { - events.offer(new PlayPauseEvent(true)); - } else if (cmd.equals("PAUSE")) { - events.offer(new PlayPauseEvent(false)); - } else if (cmd.equals("STEP")) { - events.offer(new StepEvent()); - } else if (cmd.equals("FASTER")) { - setSpeed(fasterSpeed(speed)); - } else if (cmd.equals("SLOWER")) { - setSpeed(slowerSpeed(speed)); - } else if (cmd.startsWith("BACK")) { - double seconds = Double.parseDouble(cmd.substring(4)); - double pos = log.getPositionFraction() - seconds/total_seconds; - events.offer(new SeekEvent(pos)); - } else if (cmd.startsWith("FORWARD")) { - double seconds = Double.parseDouble(cmd.substring(7)); - double pos = log.getPositionFraction() + seconds/total_seconds; - events.offer(new SeekEvent(pos)); - } else { - System.out.println("Unknown remote command: "+cmd); - } - } catch (IOException ex) { - } - } - } - } - - String getOutputFileFromDialog() - { - JFileChooser chooser = new JFileChooser(); - int res = chooser.showSaveDialog(this); - if (res != JFileChooser.APPROVE_OPTION) - return null; - return chooser.getSelectedFile().getPath(); - } - - void openDialog() - { - doStop(); - int res = jfc.showOpenDialog(this); - if (res != JFileChooser.APPROVE_OPTION) - return; - - try { - setLog(jfc.getSelectedFile().getPath(), true); - } catch (IOException ex) { - System.out.println("Exception: "+ex); - } - } - - void savePreferences() throws IOException - { - if (currentLogPath == null) - return; - - String path = currentLogPath+".jlp"; - - FileWriter fouts = new FileWriter(path); - BufferedWriter outs = new BufferedWriter(fouts); - - ArrayList bookmarks = js.getBookmarks(); - for (JScrubber.Bookmark b : bookmarks) { - String type = "PLAIN"; - if (b.type == JScrubber.BOOKMARK_LREPEAT) - type = "LREPEAT"; - if (b.type == JScrubber.BOOKMARK_RREPEAT) - type = "RREPEAT"; - outs.write("BOOKMARK "+type+" "+b.position+"\n"); - } - - outs.write("ZOOMFRAC "+js.getZoomFraction()+"\n"); - - for (Filter f : filters) { - outs.write("CHANNEL " + f.inchannel+" "+f.outchannel+" "+f.enabled+"\n"); - } - outs.close(); - fouts.close(); - } - - Filter addChannelFilter(String channel, boolean enabledByDefault){ - Filter f = new Filter(); - f.inchannel = channel; - f.outchannel = channel; - if (filteredPattern== null) - f.enabled = enabledByDefault; - else - f.enabled = !(invertFilteredPattern ^ filteredPattern.matcher(channel).matches()); - filterMap.put(f.inchannel, f); - filters.add(f); - Collections.sort(filters); - filterTableModel.fireTableDataChanged(); - return f; - } - - void loadPreferences(String path) throws IOException - { - BufferedReader ins; - - js.clearBookmarks(); - filterMap.clear(); - filters.clear(); - - try { - ins = new BufferedReader(new FileReader(path)); - } catch (FileNotFoundException ex) { - // no error; just a no-op - return; - } - - String line; - while ((line = ins.readLine()) != null) { - String toks[] = line.split("\\s+"); - - if (toks[0].equals("BOOKMARK") && toks.length==3) { - int type = JScrubber.BOOKMARK_PLAIN; - if (toks[1].equals("RREPEAT")) - type = JScrubber.BOOKMARK_RREPEAT; - if (toks[1].equals("LREPEAT")) - type = JScrubber.BOOKMARK_LREPEAT; - - js.addBookmark(type, Double.parseDouble(toks[2])); - } - if (toks[0].equals("CHANNEL") && toks.length==4) { - Filter f = filterMap.get(toks[1]); - if (f == null) { - f = new Filter(); - f.inchannel = toks[1]; - f.outchannel = toks[1]; - filterMap.put(toks[1], f); - filters.add(f); - } - f.outchannel = toks[2]; - f.enabled = Boolean.parseBoolean(toks[3]); - if (filteredPattern != null) //disable if either the saved value, or the filter say it should be disabled - f.enabled = f.enabled && !(invertFilteredPattern ^ filteredPattern.matcher(f.inchannel).matches()); - - } - if (toks[0].equals("ZOOMFRAC")) - js.setZoomFraction(Double.parseDouble(toks[1])); - } - - filterTableModel.fireTableDataChanged(); - } - - void populateChannelFilters() - { - try { - long logStartUTime = -1; - while (true) - { - Log.Event e = log.readNext(); - if (logStartUTime<0) - logStartUTime = e.utime; - if (e.utime-logStartUTime >30*1e6 ){ //only scan through the first 30sec of the log - break; - } - - Filter f = filterMap.get(e.channel); - if (f == null) { - addChannelFilter(e.channel, !invertFilteredPattern); - } - - } - } catch (EOFException ex) { - //System.err.println("Breaking at end of log"); - } catch (IOException ex) { - System.err.println("Exception: "+ex); - } - try{ - //rewind to beginning of the log - log.seekPositionFraction(0); - } catch (IOException ex) { - System.err.println("Exception: "+ex); - } - } - - void setLog(String path, boolean startPlaying) throws IOException - { - if (currentLogPath != null) - savePreferences(); - - currentLogPath = path; - log = new Log(path, "r"); - logName.setText(new File(path).getName()); - - try { - Log.Event e = log.readNext(); - timeOffset = e.utime; - playButton.setEnabled(true); - stepButton.setEnabled(true); - fasterButton.setEnabled(true); - slowerButton.setEnabled(true); - js.setEnabled(true); - - log.seekPositionFraction(.10); - Log.Event e10 = log.readNext(); - - log.seekPositionFraction(.90); - Log.Event e90 = log.readNext(); - - total_seconds = (e90.utime - e10.utime)/1000000.0 / 0.8; - System.out.printf("Total seconds: %f\n", total_seconds); - - log.seekPositionFraction(0); - - } catch (IOException ex) { - System.out.println("exception: "+ex); - } - - loadPreferences(path+".jlp"); - if (startPlaying) - doPlay(); - else{ - populateChannelFilters(); - } - } - - void setPlaying(boolean t) - { - playButton.setText(t ? "Pause" : "Play"); - - stepButton.setEnabled(!t); - } - - // the player can stop automatically on error or EOF; we thus have - // a potential race condition between auto-stops and requested - // stops. - // - // We protect these two with 'sync'. - void doStop() - { - PlayerThread pptr; - - synchronized(sync) { - if (player == null) - return; - - pptr = player; - pptr.requestStop(); - } - - try { - pptr.join(); - } catch (InterruptedException ex) { - System.out.println("Exception: "+ex); - } - } - - void doPlay() - { - if (player != null) - return; - - player = new PlayerThread(); - player.start(); - } - - void doStep() - { - if (player != null) - return; - - player = new PlayerThread(stepChannelField.getText()); - player.start(); - } - - void doSeek(double ratio) - { - assert (player == null); - - if (ratio < 0) - ratio = 0; - if (ratio > 1) - ratio = 1; - - try { - log.seekPositionFraction(ratio); - Log.Event e = log.readNext(); - log.seekPositionFraction(ratio); - js.set(log.getPositionFraction()); - - lastSystemTime = 0; // reset log-play statistics. - updateDisplay(e); - } catch (IOException ex) { - System.out.println("exception: "+ex); - } - } - - long lastEventTime; - long lastSystemTime; - - void updateDisplay(Log.Event e) - { - if (show_absolute_time) { - java.text.SimpleDateFormat df = - new java.text.SimpleDateFormat("yyyy.MM.dd HH:mm:ss.S z"); - Date timestamp = new Date(e.utime / 1000); - timeLabel.setText(df.format(timestamp)); - } else { - timeLabel.setText(String.format("%.3f s", (e.utime - timeOffset)/1000000.0)); - } - posLabel.setText(""+e.eventNumber); - - long systemTime = System.currentTimeMillis(); - double dt = (systemTime - lastSystemTime)/1000.0; - if (dt > 0.5) { - double actualSpeed = (e.utime - lastEventTime) / 1000000.0 / dt; - lastEventTime = e.utime; - lastSystemTime = systemTime; - - actualSpeedLabel.setText(String.format("%.2f x", actualSpeed)); - } - } - - class PlayerThread extends Thread - { - boolean stopflag = false; - String stopOnChannel; - - public PlayerThread() - { - } - - public PlayerThread(String stopOnChannel) - { - this.stopOnChannel = stopOnChannel; - } - - public synchronized void requestStop() - { - stopflag = true; - } - - public void run() - { - long lastTime = 0; - long lastDisplayTime = 0; - long localOffset = 0; - long logOffset = 0; - long last_e_utime = 0; - - double lastspeed = 0; - - synchronized (sync) { - setPlaying(true); - } - - try { - while (true) - { - synchronized(this) { - if (stopflag) break; - } - - Log.Event e = log.readNext(); - - if (speed != lastspeed) { - //System.out.printf("Speed changed. Old %12.6f new %12.6f\n", - // lastspeed, speed); - logOffset = e.utime; - localOffset = System.nanoTime()/1000; - lastspeed = speed; - } - - long logRelativeTime = (long) (e.utime - logOffset); - long now = System.nanoTime(); - long clockRelativeTime = now/1000 - localOffset; - - // we don't support playback below a rate of 1/1024x - long speed_scale = (long) Math.max(1, (speed*1024.0)); - long waitTime = (1024*logRelativeTime/speed_scale - clockRelativeTime); - - long waitms = waitTime / 1000; - waitms = Math.max(0, waitms); - - /*System.out.printf("Now 0x%016X ns, %12.6fx playback, %8d/1024 playback, %10dus rel log time, %10dus, %10dus rel clock, "+ - "wait %10dus (%10dms)\n", - now, - speed, - speed_scale, - logRelativeTime, - 1024*logRelativeTime/speed_scale, - clockRelativeTime, - waitTime, - waitms);*/ - - last_e_utime = e.utime; - - try { - // We might have a very long wait, but - // only sleep for relatively short amounts - // of time so that we remain responsive to - // seek/speed changes. - while (waitms > 0 && !stopflag) { - - if (waitms > 50) { - Thread.sleep(50); - waitms -= 50; - } - else { - Thread.sleep(waitms); - waitms = 0; - } - } - } catch (InterruptedException ex) { - System.out.println("Interrupted"); - } - - // During the sleep, other threads might have - // run that have asked us to stop (a - // jscrubber.userset in particular); recheck - // the stop flag before we blindly proceed. - // (This ameliorates but does not solve an - // intrinsic race condition) - synchronized (this) { - if (stopflag) - break; - } - - Filter f = filterMap.get(e.channel); - if (f == null) { - f = addChannelFilter(new String(e.channel), !invertFilteredPattern); - } - - if (f.enabled && f.outchannel.length() > 0) - zcm.publish(f.outchannel, e.data, 0, e.data.length); - - js.set(log.getPositionFraction()); - - // redraw labels no faster than 10 Hz - long curTime = System.currentTimeMillis(); - if (curTime - lastDisplayTime > 100) { - updateDisplay(e); - lastDisplayTime = curTime; - } - - if (stopOnChannel != null && e.channel.startsWith(stopOnChannel)) { - stopflag = true; - break; - } - } - } catch (EOFException ex) { - stopflag = true; - } catch (IOException ex) { - System.out.println("Exception: "+ex); - stopflag = true; - } - - synchronized (sync) { - setPlaying(false); - player = null; - } - } - - } - - class FilterTableModel extends AbstractTableModel - { - public int getRowCount() - { - return filters.size(); - } - - public int getColumnCount() - { - return 3; - } - - public String getColumnName(int column) - { - switch(column) - { - case 0: - return "Log channel"; - case 1: - return "Playback channel"; - case 2: - return "Enable"; - } - return "??"; - } - - public Class getColumnClass(int column) - { - switch (column) - { - case 0: - case 1: - return String.class; - case 2: - return Boolean.class; - } - - return null; - } - - public Object getValueAt(int row, int column) - { - Filter f = filters.get(row); - switch(column) - { - case 0: - return f.inchannel; - case 1: - return f.outchannel; - case 2: - return f.enabled; - } - return "??"; - } - - public boolean isCellEditable(int row, int column) - { - return (column==1) || (column==2); - } - - public void setValueAt(Object v, int row, int column) - { - Filter f = filters.get(row); - - if (column == 1) - f.outchannel = (String) v; - if (column == 2) - f.enabled = (Boolean) v; - } - } - - public static void usage() - { - System.err.println("usage: zcm-logplayer-gui [options] [log-name]"); - System.err.println(""); - System.err.println("zcm-logplayer-gui is the Lightweight Communications and Marshalling"); - System.err.println("log playback tool. It provides a graphical user interface for playing logfiles"); - System.err.println("recorded with zcm-logger. Features include random access, different playback "); - System.err.println("speeds, channel suppression and remapping, and more."); - System.err.println(""); - System.err.println("Options:"); - System.err.println(" -u, --zcm-url=URL Use the specified ZCM URL"); - System.err.println(" -p, --paused Start with the log paused."); - System.err.println(" -f, --filter=CHAN Disable channels that match the regex in CHAN."); - System.err.println(" (default: \"\")"); - System.err.println(" -v, --invert-filter Invert the filtering regex. Only enable channels"); - System.err.println(" matching CHAN."); - System.err.println(" -h, --help Shows this help text and exits"); - System.err.println(""); - System.exit(1); - } - - public static void main(String args[]) - { - JFrame f; - - // check if the JRE is supplied by gcj, and warn the user if it is. - if(System.getProperty("java.vendor").indexOf("Free Software Foundation") >= 0) { - System.err.println("WARNING: Detected gcj. The ZCM log player is not known to work well with gcj."); - System.err.println(" The Sun JRE is recommended."); - } - - String zcmurl = null; - String logFile = null; - boolean startPaused = false; - int optind; - String channelFilterRegex = null; - boolean invertChannelFilter = false; - for(optind=0; optind"); - System.exit(1); - } - - try { - new Logger(args[0]); - } catch (IOException ex) { - System.out.println("Err: failed to start logger"); - System.exit(1); - } - - while (true) { - try { - Thread.sleep(100000); - } catch(Exception ex) {} - } - } -} diff --git a/tools/java/zcm/logging/Transcoder.java b/tools/java/zcm/logging/Transcoder.java deleted file mode 100644 index aad8c3754..000000000 --- a/tools/java/zcm/logging/Transcoder.java +++ /dev/null @@ -1,316 +0,0 @@ -package zcm.logging; - -import java.io.*; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.HashMap; - -import zcm.zcm.*; -import zcm.spy.*; -import zcm.util.*; - -public class Transcoder -{ - private ZCMTypeDatabase handlers; - - private Log input; - private Log output; - private TranscoderPlugin plugin = null; - - private int numEventsWritten = 0; - - private boolean verbose = false; - private boolean done = false; - - public Transcoder(Log output, Log input, Constructor pluginCtor, boolean verbose) - { - this.input = input; - this.output = output; - try { - this.plugin = (TranscoderPlugin) pluginCtor.newInstance(); - System.out.println("Found plugin: " + this.plugin.getClass().getName()); - } catch (Exception ex) { - System.out.println("ex: " + ex); - ex.printStackTrace(); - System.exit(1); - } - this.verbose = verbose; - - if (this.verbose) System.out.println("Debugging information turned on"); - - System.out.println("Searching for zcmtypes in your path..."); - handlers = new ZCMTypeDatabase(); - if (this.verbose) handlers.print(); - } - - private void verbosePrintln(String str) - { - if(verbose) System.out.println(str); - } - - private void verbosePrint(String str) - { - if(verbose) System.out.print(str); - } - - private void messageReceived(ZCM zcm, String channel, long utime, - ZCMDataInputStream dins, Log.Event ev) - { - try { - int msgSize = dins.available(); - long fingerprint = (msgSize >= 8) ? dins.readLong() : -1; - dins.reset(); - - Class cls = handlers.getClassByFingerprint(fingerprint); - - if (cls == null) { - verbosePrintln("Unable to find class on channel " + channel - + " based on fingerprint " + - Long.toHexString(fingerprint)); - return; - } - Object o = cls.getConstructor(ZCMDataInputStream.class).newInstance(dins); - - ArrayList events = null; - events = this.plugin.transcodeMessage(channel, o, utime, ev); - if (events == null || events.size() == 0) return; - numEventsWritten += events.size(); - for (Log.Event e : events) - output.write(e); - - } catch (Exception e) { - System.err.println("Encountered error while decoding " + channel + " zcm type"); - e.printStackTrace(); - } - } - - public void run() - { - Runtime.getRuntime().addShutdownHook(new Thread() { - public void run() { - done = true; - System.out.print("\b\b "); - try { - output.close(); - System.out.println("Transcoded " + numEventsWritten + " events"); - } catch (IOException e) { - System.err.println("Unable to close output file"); - } - System.out.println("Cleaning up and quitting"); - } - }); - - System.out.println("Transcoding logfile"); - - long lastPrintTime = 0; - while (!done) { - try { - Log.Event ev = input.readNext(); - Double percent = input.getPositionFraction() * 100; - long now = System.currentTimeMillis() * 1000; - if (lastPrintTime + 5e5 < now) { - lastPrintTime = now; - System.out.print("\rProgress: " + String.format("%.2f", percent) + "%"); - System.out.flush(); - } - messageReceived(null, ev.channel, ev.utime, - new ZCMDataInputStream(ev.data, 0, - ev.data.length), ev); - } catch (EOFException ex) { - System.out.println("\rProgress: 100% "); - break; - } catch (IOException e) { - System.err.println("Unable to decode zcmtype entry in log file"); - } - } - } - - public static class PluginClassVisitor implements ClassDiscoverer.ClassVisitor - { - public HashMap plugins = new HashMap(); - - private ZCMTypeDatabase handlers = null; - - public PluginClassVisitor() - { - ClassDiscoverer.findClasses(this); - } - - public void classFound(String jar, Class cls) - { - Class c = cls; - if (!c.equals(TranscoderPlugin.class) && TranscoderPlugin.class.isAssignableFrom(c)) { - try { - Constructor ctr = c.getConstructor(); - TranscoderPlugin plugin = (TranscoderPlugin) ctr.newInstance(); - plugins.put(plugin.getClass().getName(), ctr); - } catch (Exception ex) { - System.out.println("ex: " + ex); - ex.printStackTrace(); - } - } - } - - public void print() - { - if (plugins.keySet().size() == 0) { - System.err.println("No plugins found"); - return; - } - - if (handlers == null) - handlers = new ZCMTypeDatabase(); - - for (String name : plugins.keySet()) { - System.out.println("Found " + name + " that handles:"); - Constructor ctr = plugins.get(name); - try { - TranscoderPlugin plugin = (TranscoderPlugin) ctr.newInstance(); - for (Long l : plugin.handleFingerprints()) { - Class cls = null; - try { - cls = handlers.getClassByFingerprint(l); - System.out.println("\t " + cls.getName()); - } catch (Exception e) { - System.out.println("\t fingerprint: " + l); - } - } - } catch (Exception ex) { - System.out.println("ex: " + ex); - ex.printStackTrace(); - } - } - } - } - - public static void usage() - { - System.out.println("usage: zcm-log-transcoder [options]"); - System.out.println(""); - System.out.println(" Reads zcm traffic out of a log and transcodes"); - System.out.println(" the data via a Transcoder plugin. Check out the"); - System.out.println(" TranscoderPlugin interface to make a plugin that"); - System.out.println(" runs with this program"); - System.out.println(""); - System.out.println("Options:"); - System.out.println(""); - System.out.println(" -i, --input= Input log file."); - System.out.println(" -o, --output= Output log file"); - System.out.println(" -p, --plugin= Transcoder plugin to use"); - System.out.println(" --list-plugins List available transcoder plugins"); - System.out.println(" -h, --help Display this help message"); - } - - public static void main(String args[]) - { - String infile = null; - String outfile = null; - String pluginName = null; - - boolean list_plugins = false; - boolean verbose = false; - - int i; - for (i = 0; i < args.length; ++i) { - String toks[] = args[i].split("="); - String arg = toks[0]; - if (arg.equals("-i") || arg.equals("--input")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - infile = param; - } else if (arg.equals("-o") || arg.equals("--output")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - outfile = param; - } else if (arg.equals("-p") || arg.equals("--plugin")) { - String param = null; - if (toks.length == 2) param = toks[1]; - else if (i + 1 < args.length) param = args[++i]; - if (param == null) { - System.out.println(toks[0] + " requires an argument"); - usage(); - System.exit(1); - } - pluginName = param; - } else if (arg.equals("--list-plugins")) { - list_plugins = true; - } else if (arg.equals("-v") || arg.equals("--verbose")) { - verbose = true; - } else if (arg.equals("-h") || arg.equals("--help")) { - usage(); - System.exit(0); - } else { - System.err.println("Unrecognized command line option: '" + arg + "'"); - usage(); - System.exit(1); - } - } - - if (list_plugins) { - System.out.println("Searching path for plugins"); - PluginClassVisitor pcv = new PluginClassVisitor(); - pcv.print(); - System.exit(0); - } - - // Check input getopt - if (infile == null) { - System.err.println("Please specify input log file"); - usage(); - System.exit(1); - } - - // Check output getopt - if (outfile == null) { - System.err.println("Please specify output log file"); - usage(); - System.exit(1); - } - - if (pluginName == null) { - System.err.println("Please specify transcoder plugin"); - usage(); - System.exit(1); - } - - System.out.println("Searching path for plugins"); - PluginClassVisitor pcv = new PluginClassVisitor(); - Constructor pluginCtor = pcv.plugins.get(pluginName); - if (pluginCtor == null) { - System.err.println("Unable to find specified plugin"); - System.exit(1); - } - - // Setup input file - Log input = null; - try { - input = new Log(infile, "r"); - } catch(Exception e) { - System.err.println("Unable to open " + infile); - System.exit(1); - } - - // Setup output file. - Log output = null; - try { - output = new Log(outfile, "rw"); - } catch(Exception e) { - System.err.println("Unable to open " + outfile); - System.exit(1); - } - - new Transcoder(output, input, pluginCtor, verbose).run(); - } -} diff --git a/tools/java/zcm/logging/TranscoderPlugin.java b/tools/java/zcm/logging/TranscoderPlugin.java deleted file mode 100644 index 8c6f62c61..000000000 --- a/tools/java/zcm/logging/TranscoderPlugin.java +++ /dev/null @@ -1,18 +0,0 @@ -package zcm.logging; - -import java.util.ArrayList; - -import zcm.logging.*; - -public abstract class TranscoderPlugin -{ - // return the array of fingerprints you can handle printing - public abstract Long[] handleFingerprints(); - - // return an ArrayList of Log.Events to be written to the new log instead - // of the Log.Event input argument - public abstract ArrayList transcodeMessage(String channel, - Object o, - long utime, - Log.Event ev); -} diff --git a/waf b/waf index fa5cf13c0..71ff7f327 100755 --- a/waf +++ b/waf @@ -33,12 +33,12 @@ POSSIBILITY OF SUCH DAMAGE. import os, sys, inspect VERSION="2.1.6" -REVISION="3f89fb80cf4aecef352acb956ba1d194" -GIT="e3d38c18cace0d30f0ad20c6e8c95248fce6bea4" +REVISION="d005a5204ce120a48d43141bd13fca5e" +GIT="e41bd27b6ae7b1dd21da6c25d13601072a565d80" INSTALL='' -C1='#8' +C1='#9' C2='#7' -C3='#4' +C3='#%' cwd = os.getcwd() join = os.path.join @@ -172,5 +172,5 @@ if __name__ == '__main__': Scripting.waf_entry_point(cwd, VERSION, wafdir) #==> -#BZh91AY&SY:U#4ύM(#84uHb)^7#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#4#45o.[ig;`{Ujj:}tmwҵbQۦ淝n%mMYS|o>r#g0CFvnZ W-ޡ#88Q5G8u&e緞vc\}#4#4#4@: 44{#47<^H:z=:P@;٭n(Ow4CPR5T#4%#8#4^B)#4@:\^;}mEwf)ќioݑ[_okG6Ƿ⤣]mJ:sG6;ǽ.=޺u}s`` JA`ꆪ6{w=es=qh46Xh2D'7=4#4UT #7fy:uqn{z/j'#7^=}$#4$_Xש ؼaЫë3Wx{#87 ;w}}^w]7c!Oyٶۡ˶nm6rNL_w6yʶ];mz{&+^wW'{+xOI{wo fOWZgǛRytǷy}GS^^'dz=l8ۋNcl#4]՗w{|f`Q*#8Eg[ i{=skN.cwUiM('+ml\wmGB*w7#43#4P9ՍSw]w sۭ篾tۯz͏s95@=zof.T#7r"q^}v[ U@b7faG]ݚN kJݼ}f7j{combBڻw׾ l2>tݶꡮpj DŝO;u\3kuy.v#4igPI[۽k;{hcL*ݞ4}KnvӲ[ۭI[Ǫ鮤#8C@{Eݵz݇zmڨ tw7z{gxѦG=ﯝtm] PJ#4=-l7.nf3V}n2\ZShu+y2xF^}ױޞfϦ*M_Y]}ōlmݙmO0y{z{P|gWwƾ;pD#4@#4@#7h0C!MzyLP~O4Se)B &L"=4OS66G#4#4#4#44#4H @ ІT*{i$Tj=Gz4#4#4#4#4I0hC&LOTɦ #4#7@4S@#4#7#4#4#4#4@#4h&i1#70LODd4#44#4#7#4@dz&2S䞦#7FꞦ@#4#4#44#4hP* D#4?#4O}V6iU6 o*-[-|5mr|@?)Կ)y`WiT^&!1xtMu8u$R#45_nt#4NWb! kJyUU)”@O#8V(WsS3D'1p'tl̙MqKB3AQUH(#4H#8 UՓXV5j6[ !"QG&E$A$DlAOT DP$@d#h#4AQPBQi$e0,3*K#4J)$55MEB3HH-2(R&Қ #4Jƥ M1eMDRlK-4Ҵ4QŤF64E f4fHRM6D֛Vj%̓ ( &[i`MIIY`R3S-&&d U4h)#h@٤m&,X4TF!#8)" ZI$m ,D$,a! ̊YY` % dƋL$5$2YHHѴMJLb#8L̘ZJ4DF #7*#8$D̥d%3Ɗ ؄iL!"m%%+!#4I 6* I4&D`E#"PbIHF %)&EQRT$BZRa"IKbH̢fID٦F *&I6D3lB4L,h̙"DjVK$llLA5"##7d#7JR6 "PɚEb)&!,fJI$!CQbQ+(Rc`#H$&#$ El-3-f()F4D̊jF!H+(b)$$ccMM"4X%33@)2"(fM3 1E!F͊4ԩ2f!dd2)HQE*I4I&4HHb&QSJP2f BiF#cLiJb,!*KSSa6K#7%2EEPT%F"L#7H҉mHR1IjHIL"hҘ(4%24imm&P)b$dDVR`RbH!e6EEmd!E6#)DL4@7۱FحjL)*4M6a$mdChi2Edb Ve5JJZ+a5-1bŐS-62i+Fʴ4"L*K%HSfc3VƋ"+dVflY*e)K26jFmEk,Ii ڊS F* F&j+"QDJHMh5ekY2aF U@iR P"cFH5mREC!2-IMDJ6Mk")YKe6XLF5,5MM!mLbA*RYd4Ub 6HcC4LD Hɖ,Pɶ6i,ZF3eE6RVlJQ)LJ6ih M12PII#7ђ$d# F &bɤK $K*P504i1 MD$&ѐ(̵ѳ)$ѐ#7E")hѩ6McdK $j)bM,TM,̓FJ blClfmњ"ѩJMclAP$J&0 bRT5)ș%3Q fc(,JiE%FՑafh1T4mQ%IS1 !"I2daE1V36I dlb 6Pm,زXC&,R"*(D+TI0Jj I*A4,"RV544S(-&U"e5TѰ[LdRkH@(FMM"$EX(dFb5lͲX6# Ta`Rb%m33% Қ%j4j)ƉBԕĤ6LE02JԈEEhVTe(Ml!j6Ƥ"!&FdDLfȌJLZ6j+dfMhKR-DlQ$mQL#75656)D%ʊ&k4m6TRei((L%(*HڊزmZ4iѓ,lmUKZ"eBbJ# $-&SI**c$FضŦj1eRɶ jMME)i#71Al͖j"6 #i&S6ُ4dB9ܨΘbG?].~6%!pِ?|^#77'V&UM&'?$:eU_.,7!ToN:-"0txHF,$(2#8% ;Zy4N}[dA$>oSLuKA}G8'%3N-k0Oug$֡Ww_T66>z(0:> +UیZa'{#8lhF Ed;޹zy鷨j,+y/l6#7 6.(!7zfe;̡LRJHHNDF."#ΑK8#8"/rf`2`ga1UG+THZ{ne6|ɯ%X_wkITc{>tzf;[E0Ŵ)U!DS64@U&w\´hc72##76Z6dKKGmzİ3ۼΗEYͺTQW6bfF}_)PW\YE2IWᴦR~VG>xɖ#83lǦ m6%^-gUr(<$7FVMm&wܣrLCSLEV$2fR*MDTR",0!M te>x#8#8Pchտquِ∕FMh[-TTX…(TJ\ 9恎φ}F"fY.d;VGFv&PfSCjPPmxY|G'V608kśb&/z3(fV9wj˳ވ|YH)ɨ>~`'nn#8aQTp)!X#7ޜ:2!7#8_{2ͪ~>)YTҰ<(TGSCFp"?FChHKxYw{0Tj*;h,YGC2b_]|j(o.#cJU.bЫULA@5*Ұz2쩓ZP( biZDVѨ%\PSYBd()bݯ_׀.)#7PJZP|?k3MF |H #866zFm̌"W#8#4f"jɨ{wkI(Ѩ)bÕh:UPu*-&5#8XנMjS4D/Tݼ#G^)Mgt}mBk|M4a}8yh.'t+|^,$4]=udB)-W}KZ.嗧u REF#8(SfZjASzw?tQDbmZD¸!h:QENQUΧ/΂E"'|ᮁ -irac;|G=a̪˄xƻHŇo^-#7Ѯb2f)/vfN,F t@eW$0檝YfRE,?\Ay>rEH~<>t8!>Ni&$\ yq!gF6s~Ftf5a쩲cj:@dq~ I#7Yjƌ3vy#(^ǖtÊF\^/W`+#4(l;&<5Л XSGq9땰<~gnekj)Fnwwk:68)\3نhh`q6pݭu{@7rnHc2zeH,Ӡ٦;xN>֫!x9lXzݺ4ycz"}l)W,#I^IdpoB8dkrlhfGm={àA<[O'Di4 GG]6v7 $,Q}§*Rxcƛyݭsw'_ÀN#3 Ѷĉ,y#8PTN'11N#7xr"ߔ:#mƷJf6l}z걲/"Q(CCz޼U|}YjTݍ}~Fٿ>sLa*cl,BuHv}#7__WRe$0џ]P-0jTְTQ#4F_>:Z|1ҩ53|jwEQۛ7i?7jңiTw!1i}Tר$.)~un9cevԡ#8 g\TW88iKΐ J#8R}*!(DDKg~dmAzQ;kk8X~u5:y_B>?.'ðXOLT҆ %1Q#6; M0lMw!YWl}Jǎ2+@ߤ?Rgzs$(ep;hwLx;:x^:-j|&ĈV̲Oc4"gW"'^̉|0S!QIjLòj?T*sڜFzB7լ~>|c>l'.q#80QG֒μ44ES#8UHx3s:Z(OvpE'$&t޸1έ""celG&HxJãt|4QZQbtMKhݔ#8Eͭs蜬hPmЉlaKR,M͕л(WlLAN;?E=ZY@y??j,GC 1_hgNȮ vsDUJR"4Zf^S~'zw\ԭqe0,!:Lk!3zzEc_ITˤǒ0ꐨkY>:P*#7ͮscN/y^l1_O6@2$%]H~;På5(.EU {Uċĝ(H뜳ËٓVDyN<*@n}'P JҿT3{ @60Rq)QΩfגּg&Ry;]M\4# *-n9Yzũ֟hb#2~ŞAXrKrU~1#gj.4e'L^\`>/6Sc`{>.Unր;Әu< ֘&IӚO-=QZuW.7/-0(zi@pz=y\(U#8(͝xZ0P^/_i^L#8Ux36:KMZ{^02s( VV݇z-p{x%g3ώY|btxxMl!~~UJ*6J8M>5;Zcdֺ#8, jܘcM7Xe[iUYWMr(#8)˄9qTjs[OUJ&ծN:J?cl>8:୻5O^Zh4)e}$X"y q_P?TE~L#8t4c"64}$4M+cPN鬹~9YNHcc_qҷwylG{oWS9=:E$49֭#:vmaCt#7"P#+?E,\7c{!/e^q՜K`W~.jx s$L:ťę#LeM P!wI]a&pS18l}y˷M"W#mIUӍűJ6(㓙s1k#8^nQ 3wNty`O&?dyjޘ/#8@x5sUPݥNqh)(.tbo=~\z꽮uJVTv;\\u]ruK"J4pT,~Ax~7,@3aB!̣{"/D9jsN̅5#4 \6^FrX<\AFW%1SFҵHOa9$q8#4<Ԕ:'/,D\}V}{/Zժìꄆk0Z#]F팍97j#7>#&Up墣g)#7DK(ܵoFfP@OX!Ft3ۙF9,c !eHQHa5mHfvAiN}o)@|x@o$׵Z4-reå#}nDya};΍o^Ȋ,k+N3Ӈ{]4ߤߗe't@dmQ7_H߭)ύuC!r(Y|C ]mD*\D0-K垄lK}7um~N6oz';aRLDKDy`HTG#7qunҼ\w=j\l|i@GsIDRfE|Hvae.96LX }a!t-1Ƀ۶'GĔh278YaYա?YiecOd@%࣯;*m?7s!fs,9(Ho'ȕ{25nAWuQ3ip#7HW1ǣ!F@T,mkӧΧjuŝuY>kKy%.Qy5Qg]qľI%xuN.ظ FaX9T^WJvX%޼nt҉Lx򨿯\Ο\bЁsXF{H`wdZa$#OF"|w=#7<֔M)CKw[d?mTP8DʣY*Wb"MoE/IJgXI#7{x8 rV]{I$ڎ<܉*P`zr!bb>fT9qW".KQ_ !w:6|v8{y '7Wo'| Pj2uz]0(l̍<(h9N|#4xi[J#4JD_4V#5+]UGZY"EvI#n*9G(䟭:ퟟkm-6<=ς٭6DCrU. 4kwEb{!p! ğu#4cƘJ7$C_#4*zr~QVnuWJFD;&J.A~p5C}jgP5r;~:I;p1sƨr(A>3#4U/4Zt:hfGTei.(|`G(+k t?{J?lyRuP"#4d@B *7F;]}"1-E)."̠,(MdM#8"B#4R}0u#4xƮWt#}6cSlXDd#|nAs0*΃1|661p!2GtX0u_ڷt01_33EQׇn#7/(O!}v/z#7.Ea.3xItF3M6=cn۪?_|h~xnI! ZD`/-*)2<`=.]|ƪښf"?,}ohM½8K:J#8(>]Ϣa 4%@.XTd?Լ@,"KSu$E|l|qG`WrRx,[]:dS g{&2TPr!_,NOVJ ӱPߕM~~*ׇ;VaXQ۴3f(5 ZIM(Psɟưz@ VMłdri1><'?/*2Mx<)ps SѼ#eGsΜND[:D#4T\Il\O/]w^KAё00->;j+3x[^cWbهΩ!u٨v3<(gZ/GRmNzX-%n|dYOiĞkSf#7ֽrG(}A:ao,NJY%!AuUEURRKw8#8FpaK^".WK""úd ; `ks-8bS&Of͠#4R#4bM{g靸rPhDv#4 KixI8iƲG~D4ugo:*r6l4X*+r{'#89!TڕT Xqeb+Q_8oz]%\ V붿%muĞP_ ^* Jȴ*Ap",+2ÍRi!Z8y^KI;v0-#4N: g_Z/-{46147HLkSB;XBۆ~cłA| K &NsEMJ52&XLLp $vz,XF(n0dKmy|}XHR )U)%D%m#7qee") 3K0J .W0f4wAq{q_ZiMnS9CIBc*#4N+^Zlc#4 ]*GfAă"Gʗuq$q Em3 [iZ;BF#4@(#4x N\CX!fФv4|0a{ˆaFWHqF>[Fn#7$Է+Lr.ԳPy L-,P%`Qpg`3w 29ޫ#71ѓ j Ԯ`n68Y2cJ7oQHI\^K[k-,fHh2O{<{ݑfyJ%U{A\aMZD/EҪNTSbG73:z;e݋Tu݇7A dYi˦phr39z)mBPF u ShCn,|{a,tKn$qJGaoqGO26uX$sYN%~xܓ#7$~jCx DŽ3;iCNx#7&sTµ#7P a@oAںOmWH(%lL(R5WTǺ@9;'8J*v":K˰`vEh`kp16J X%#7Uǯ:[ƴoQ"~ϺGqeJ(ؖ0FKOys8'ʿFHn{\ #8 R0J?@Q|o.U6i/*'D^~陸i,hc"KՐ+Q1B& zRX#81'GZ,! 86h)cP##7cA0Ͼ\ 6"sf8@bRvc)ѵb)c63J ӄC@F25mǦaAd`!KoY~aƐkU, <Ɗ*#7NFy\&CH~4`k\tݥٞ!?tӹ#h3MyKDBi?[<L:|/}ZیYuLJ[ .gN|c8b ǭȏm@obKdvr[yPnDhHXR+M7E+_rq9N|J0hq57|l/)amQѢbHrtoYpXLr#8+c4H"H=Fd=R_()n$,3\걟hw}6`y5IlEm ju9C];qg%8t>D#7ѥA#8g +#+g#(UHnnsRT 3#80tfۗ&Ti"1FN"MU\!sp'?#8ԘxQT|*X?{#8168H^%#8Ym}x2wV0?oA^Wُ}%pcn:K86Q+v!KpǮ8IDZDd!#7DL-kDjV(1!Dc탠y4t"evCCƠ;-aXwPYtN8= !i|(#5y\U,A@F-Z[Obvyeմ)Y[nȧS⇸,(`s|l;_Ͽ&x WM ".;#7UDٸYd?Mzz]m]>Tgi7ZAPDc#7j=ku+ "d<^(Q.~]l}VA(EH]$0*AʥY c`1O#8bbd SUqDU}_Ҁ:#4&/ZI?RPJ@J_mϦ9~18ut4Ԋ,+b3uh"%D͕,J/q4ثcbw:Ү[)jTi/X8~]7lWb e̠-*)HJ(–*A=*Mws~?cg+( N׾@_n͝#\w6u&Bׂ~Tq##g҃5nmn:BCտ'G3[ 0H; ]7e)aOIs_GN#t%wnKбt_ 6.*nxUf~~-Axỵ<-ep'"@xEn!4EiT/zWiW*QpÖp &9bgӊ h#4ᡳ;Kt/uhGF}. * &3@xH*#G;G}E(=ۜ{(Jr'!t3g39th}vq#8Wx(2&O.-I;YbBҙL4*+kB'Ρ㼄H6T$'\:>0LO2GڮH(QEπs%UTYj!@Ochҿ!x{6`#̯#"EU @Z;#7W{7:XgHYqAkDy/U#7/0L5v эzMꢸ2A1BA0äf]'CZm=}(:#aaYqN\C_Z6bF#o2OJS]GMiiV2N]꿿-DaѼRi]Ф-JB r-Y6D<,J/W:;y'k*uʛy䪃<ܷ{+l{u̳#6`]ƿ/DNn\ݯQk'26Þ{ tqBו쇢pv ?X]zyItkgS~n3׫l\=y̪юqőrϕ:g?wPz:-{#'#e g}^>lutb-Cn8BXs?C Ge e;Qǝ}ۧ7R6@k6j޷CG񕱌4ѻWR8c^}Ϩ5D.,E`n^ϐLxvNg\u$kjoeo1|ܕO͟#7o uks3u_gzwrۻvi&ͫD>T&v*t<:V23tH;J(|y*iZm:\oI}1wx et/o40ݍ5/̟DGYK|R#4ʨxxv*olwuh>p?DWsb.&:pS}g~}Vnv#7~мq{ӷž_ z;>*Rb*9ݠGտP|и(?Vq_o&{pwt?3_~6OWffȩGģǿg?4o:6ޟ1}>n p;_=Uxe!S={?#4(~kKZ^~ߋ#8*2( %#82vCpBZk0;o/3L~@jHQ[QϷS#7,*Fϳnz_FCo[qӬ~9ufLk~onD[xlblt"s}c<|${?fEg>1=syԪwCV0V[O[ X ~}#8`}ٚeJn'}ëx yskXmW.mNdiP!>A?wA!@`>6/E%bIq3${Ṽ!y͏;#76w5GnP}SJW+T獰7Li5 v)Ldpv5TzeIࢂ"i 4#4ޛ?VlO&ѷӿg^y3#7v8@P67#8o~8/2+#8q}kJ#t%^ɻNj'*4YżWu宫)|^bV|/QGNtiploMkxu3ǘ!]kA(<)BѧOhrJcrBZgFdmh/.\4,t/W&Eq8~# sMuH"!Ry$uopDk ]!;h2y#71͝;_#7ܴ#7Ç8{tmJ%n)Y© Y4"9OSah#YI 1E4 k>%ut:V .z?Yevelɓ3-n=O =G *c󍠧0:y'y~)y_Ûs'MnJvvӫ>i;=QK}>9}ϲfrMiQ/ksϭ%p#%gV3_cGp?/D#4V_`v :tS>_Ui3.dmK#7AGQh҈6ة EJ$ݱ!n,(8#8*"O،zg#7iHۄhLoGd R#8kdVDRE,hZ[^ڵ1'ц؛KabimNH I{E=x6h?{ĻegF ^]RA-#77]V޽NgR"m&XFv&b!T[V21K\TkzQ̘&0[lXSL##)*EnWކ6R! ciE4`ȣ41X䊲Fe%mPΏ2Qn0M$c 9y c] R#8WZAt0f(&0yͽ4)lcQ#7c@P8dg;Qi> f#4-g$9oC8KkJXDu`ưئrޯ#80oJ SN"G;F686"jBi\D#8JaQdeDE+2^7ysmLq+R*%{!Xr9UPY#8D=ro[+({[JQY=@1}WnTRݚ/vGOt4\#CɄC5~kYAӦ(&rryF xc&әE*Dmru7s,!9={._y}~}DN|Mof{m374^?z?t7}!KC@h=P}\G47yaT\=+8:q[}&hs'KRVڏ,5:̼!J}~^;%KV.HHD9GqܧA39(pZ['Y4) !e51ULI4nrn(bh#_?!#8HDkMF#c$#܂#7@?x4c.D2# ݊:dk>_Ыv {G|9 #4&0XF3љs+ׂc5Ƙi Kf#8FLp3P,\o@ZuB{Gy3EXk - گ$ak(fa#8LW#7/]M}x`#Bѐ1<:UX(o|&Vb!Z,$d]_ ˑʃsVƂͽZnF8Y&L54F*0eьe ,j8"Lϧ. 3'Mj#7!ڜՀQlmJ.SNlל#7Y*<)]l2nV#8$5O7+gXLF۞ 2]6T#7f~'Ӂ?"(s#+˰pǗŞ) _ih`f4˼}Ssc?BX#^}RqƊU‰͐y=uA悀h#1e!<)hqL>rh$\LR#8̰M"mQ%4QHf#8}up9$n Lr) 6~I[j#7Ѹ!K&8ft3;l7#8'FNzA~ZoeH;8ߘ8*Az pzkB߽lCoɨy9E?c[?rӻܘҍ=92/V#ɎA7\c';hR/crq4TUu*r#71vLRKein1V6DEAA&*wc ]6(#7AQo&2^^("0FTliA\MXAV)Ռ:--չlR C 69qH&9 l'scCKd#kFn80$':tɁd8]aC$>7!&#aVl۞wC2dA(᧲1^n#47#7{{(h۞CEV&s掛#LL]ͷnMfY|5W!5hj]KcnΗP<;nr67ġ/u׽spq2[[aA0j9:e&J-8Mk]+:#7n47BJ4`nԗwqFQuAY7J_;+6]7]qЬ5#4(|.XvO)tqɝ8NQZά9ctVYjILm3BXPMö6NA>(-Q[3cF2F%@,9ҵD9ANDqU(7vl^() CmҞTCjg-RxclJnqo(L(:R(=#PoZ*ŀdɡϞ+уM6 }ob~^˭)29!ǟ;/c4@4+3=VNpy-]vk}kw|>䬫xv'Y6m%gpz6{Ki׳} n.&\Z*ΎpSTÍ#8nNJʬ>\z/F3߃3g:Of+Hj"~}+ gxꀻYnh(9}Fˢ""h ;s̛|%ُg5OLH4"~b)Ju%">:"dT ę6XU#~:3#4je<ϊ5x?ɹP$ݶnGN&L1O~|i1JJe2P V݀1l< EjY8Ty~\c}rꕛ/O0sYLdUss{-#71Na_)d|3ަ[Ou'<:vbQ,dvFgt=e).2`.fS#7#4oM4*V·Yo5= 3F`6N:#7!A;S$#4Z˲Ik)&:iYGvqam: џI+3߽n7J >E<9gj{t?s*B`3S"pg!M?^]Jsiɩ'ik$o )XTi_g>lgQWG#`=! b3 XJua(oÆƂ ct8!`#@'&[u#8ŊY4K)#8#8M"j;>j;hgHuZ>reIhJDGyp4tWA teL̟[My maXq ".!C&vu١gQ.3mdžMfW)ĪB{/-|?)(#$2>?5ѡQQvN;Fmn¨P_4szPcz%xVwk_T:c* }*#g|8F_*kQm~"=h'o;NtLEEJ,Պg6\Uq zGiS8c%21&Z#oVa{ѠQZi]D*yO#8og1kS-&Es6/VgYZʊ0;o3 i";;7Z;,&2UWymQth۵;YʏFW8io)XR"S~sM$BO7^UK蝳P;!V>huZIJ̾\3)Ә}79rN/O#7tۣ#8e#ɚ\}+(D ZN*F(zS=w3(Lz3F[>0xMwo{䪤ZGAo'V߁#7%\>>)!oS_N2njޡfn \Yɪ/lykA(ɈG׃ATL,lkh*2!B?%`p[pwAؤO(*+nIlz>^u4ouceѠ n;)DBכ_TԎqQ6!>Sd͟ H&34p<5;BŚšޣz.TSnMM#8-kEoجKU9BÓn)FMo9շ"Fsal \Œ?S 7h^FZ? LVgsEg_épQi^QrJ#`eKTNE#"O3mKF3l>KM gn'jV\E0ɆHȻo6]AhɩW⠠Gg<OzlI%}_w!u Ӧ8ͻ: Oo l\&]RQlV0I,4Z?φ)*{<#Lh}B%x<'#8b̌6aГzPX_גhlr(sn l& TvL2y Ao{DXδiPF Z$|kd3pkTY@Gx[Վ, 63KF!cV9Zĺ=X1 M[μ2Ϣ>\`mP'#8qxTRi%܎yhUyt]4#x&seXFZΞnSV#8: #4ȍFxyJF(_IۃFHͷ#8=a Tx_/ )JҲh%i+83#8((#'YZ"&x(TW۫#7NOHZ<9uF#8Քn$F.#7:Ū$ CAeoJ,{tV#8z0cVcYE{$}\%<_ӌqbvYCQgJn]$xϯ+ u""n]Nwuz:\pxN4Bsg$۰* 5 LjG!+˶_IsE%\\A]wfyYAt=qCFB8!&vL`'[^ѪzqZVf)#856IDc#4mR &8k7*=Ls7<7h_IwH0-{۟%AeDoۤ2.$f5"#8aS+TG<{<#4ZBK(?ėc+*=TP=bhQ!š¢,}zPD…{ioPSL ڍ_UMg4_˦RbB⽸\{mqG)Mi#xәky6uU:52ɶeW9nό>hzYzCoH:#RxD0y^^̧#7ݒ>[8n;u~1IXT@o ϼ4^%UI?RRV琌9H[.Rx9@ il[nQEՋE5W&1l0Zǹ ̖22XlEճbҺ1@xaXڧ/pa1)U;c#7aG>Q;Yͩl0'P8?Oc،.Qb66!7ISSKszC oFynro"o~NM 8S@8Ypz?v3u-IDl7ՠrEK_m\mxSSV2Ŗic#8LxN%W췞kb0eDJi^LsaO49HhԸ5QJ?1p[J  #451RƜ6QE9i$9Ȁ ͛'YOe9Zʂt?xfcp2`Ry9%=!#4["UN9b5eV&PHUyL.HMfYXpϔsRcVhNOĊ1w7y;1D4-njB_VTVumF9p'͡U(J65éY0:ꌙ*X zd)b1V,mΓ"%^*'IY48K_Q`zîz# Yd!.喙_wloO}&L:7F,{B#v2[jz8o \#8`;J@V56>xųx%{ـȮ2Yu @6Dnz@5&0s,2qa2e63eElܢ#{ !Y0r , /K ugܣ:*ˌpNпXI9H^=yz(jxT! 0[۵ᜐFU9-;s잫~)w=[{e薎ZL_ ftG㭇&v?NJ1@#6Hlmi:":a#8yDo[#8pkz5cIX.ʽ y#7f1T%6Q9K!0џi ;rDP-uhq\2g#8sKvGƁg>-b\PZ*PZ\Oydr{Nf9XUaѥMb4bD̡#8Be'-ꆹ,$f1tms&( r[JIw`Vv+Ϝ{s={#7ubTtn[rwIg}7p;bJ\n(LI rDbMV0p&HA{61١Т]:+[+痕p^:~˜Ml>JllIAؑ?>?^;dM*=+ҼvǷha5zfQ`o$v6Lp7cȗ\^{XH LB؋ S[^]+U,oNgWI{GG:kGHo5T%(Ov+cM xEnFS[BrF^{ZAqjK]۷kP\D#AM1#4Hخ h"!К#8yN#4F9[0,0,ʶ,"+=8<FK+3tL+)."1$~jN/xŮ׸Nd8=,|VF۽\ <g''9u~/o7"O@~]@y:\kr3c\s#80ћۙSkm|w v>`U[ y}*Q#8h[ZOybP%"ȪHQAPbr2"Z" 9pDa2婈QH+?B/!rF*@dNXTM ు.Oը9e%Qq_B[v\u-V#8UYl#8h&gU,"iFN},/_菳|\a(_Ekk2lĮyC8@8HX p#7;K0S4#7-ۓCIT(aLt'Ό6d?[O)Z#4_]BI$s󈯳u#8q#7" P{=lbRhSZë-8u l0J,` Lt90غ|` A2l[Ff.;`>轎#4S2M#8S'؃H|{H#7m|_f\ XE($q60/l<4%^9=|(Wn:V+*ui@F&57/j8#8ŶzJmx}7/#8,=h;݌PI:vA8wi0E~O]ICWiǼF r_㯕W}mT4CD|/?xjEDn8 G#=wÙU:Vb}RO\K#~{f4E<}ާCFL4VX L sOiV;׶m #74f5jC~Rm6!ką4l#4q h'(s۟;7is1usGM@!ܼA7`ǩX죻떲+V#7$!hYA"qСDJT|``0Ǐe39/zRH+2w0lãբe} T.#8&{p|[+-(>9; $N#CnTߣLhcg#8#Яq#7uLMY &Z5K;+Y@Ŕ }#4{9 0ۊOW@U=eoԄbP"GiNVB`2%#m >#8IAC`yJIG O'/̊bSSB;_an tL0.:W):[xu:6,.((D̓S4.Xד'X!.:?io2(BnsZszj\5pVN䟯`"&R^{e@2,K`}Us\}8NsDPXch;9ֻc i30d3=H0;1}Ҏ:e\zR?;Z'&-wA-sA3 Y6yKHZh Hp D1Rh;OF]7 : Ai#7(nxyG"yH;4SD5wp'YVvLŦ#7 @rY8쀢/4l %#oE ܮPءaiB"0!0X-?uzpIpwhL:[nyedTB@f 0Mj0fR %Q#7A2-$䙨 BTZ/y(й.D ]q0h^\,E8ieʄTaa/Ex9MW0w9|xe|1RL&l&z%Ë #8B23ŝzȎ/Mq厽aQՌ5l\UcR  ir b"Fa8t:㘅~p&Gm5|=^RG]Ѽ^\<;B' x%d;GٛPºﯮ}Z)wjO?@#401(%6fH#s[ZpهRX0z 0FD,7za+Vu죳)VZ$L719waѣV2bD֙|'`_Q<.6/Ʊ"v]W^Fo P`)vk!J81ʠ"b;Hx \dB1^sBvr#8"6ͯAoN-if#8l;r]pmh03ôFzX#8H͉_oD0I#ȯo{mt%^^7t:pK@QbQ)AK'yC k_N#4@Lo ]`V$5(sCf%@žyC88MmL#d쏆*L%=p$.6iD"4Bg^폖ç9Bn&SQOEm :1#7gz3[aCep!]oDlk{|~\uj>JbGǦj ?zb_!·nR..1DY"7=@i,#7?xu(3#8!^?OƲ3Ƃ#4'<=݇GqW$|3TB qx!".O҃oڜ)z !4=v;*`DrD#4-'~9:9y\ep)0"Pή;vLo̿UFN[Hlӂ,rtc磘Ca~{jPJ}~^_(lċu4C;{1٪"eW>DU4" d Me=ֽz=L0fpi-PmX#8!˓ˮ.a'Ŧ9[QPWt,tJn)*:;h[#7ٔ9+F$ }~=8\SzB˜SnAĀ"%Ǐe{|og߿o/?_=^_//~w?Ox#4?GX$E?Rh0??H~҄?9B˚oKhSQrG#8(]!f"D& g.B`Wh#89B (S@k.~#85)$JBs;,Q.$&01#7[&ģMK͘A1,8HE1%/[ G$8#8xHj/l E#8Ń#4=0s8UP(l.CsՍ8MtPI,86,36V)D`L˥瞪(%/{Fݦ{x>ymGXBW{E>TEzYn#7U#4m`?R^nϙ#z I e\1vi ͺJ0פԆyzFؗ9v}t)SSKZP㾭7&6hauIJgHnz@FI;=2Ȏ/[[k5YNi>OSa;RY]O eeW律V;!>p!hеpccN662͡||ڕ=2v:hrp1/X{6K*Lҥ0-䌹(4U']bY];F[`9P=o8&8v)Ar&[p{l2bW0>G^ X4@g^ew:H#8#8Х#4k5{|LԬxكw= Ͻ4:vn5Lj{rToMaP; vBx>l$HgM3" $]#8ݻ;6"Ft9^:GӠS7fP}5Zf45I$l:#7uffA[&:sM΃atҚ¥k%Gկ÷vQlTX{484䏦L} 7tjVbX!źp|lO#44#7.sWm͗geVUFi&`Q1ACS S^X!+ tϖ{h1{c|H܎9#d_v&SK)v)+LE]6#^ %A}|83ϢA83740OTX޲h'HBԾ^_ĐM@mB#hx{#4hp/#85rc|ֺ#YA}禅4M8^:?z'ࣔ BEyzedM90AIplMu8`mCENR)@* $͉Ti4e,eF&nRLdK;n}x8 *JHq6{|ɣ0ۢ-wHvix#P#4eOp z}W:QvS*.EX#7X`ށ)`w+TpB¯z6  0fK0Nsҗ|?;#8%9z1Ш~\G$={,M̡a$)J4^wj .? B0 o@oGuH?[ V'F^X#4[d0[~6,o8flF @ּ\I>gM{${PA /i8FЧ(0u4#8,G#Jj܀8FKU%Mp#4#4n 6lz֭W:k!ml_?yo|:ugoO+s+ $dOY]ɼ{!ϻ)oX#8+L)!GՏN#4%s3$-dAS?h'o&s'#&6:0cŬHppƜ?Kmɲ;H]+V#8"ߟ|gf#4\/#4z[1~T:DSJEol@0=Dzo_#8BgB}5D-#83}lટ^7|1ێ'UNvoi`HryГ#8d'0D.b?BP~ agpOɂ׺ 3g4o¢BTJExp>"g0 m4X( >ѬSj(~h@#4?a{oݺ#8~#4AnˀvHsfy#7ܨocije#7B5 p#8;= JL+D$(=2tײٯuѼ_TLȹy[OYч$чlR}(HbDH#!}˵*()'! ױ@J.ճ[#7[Dda"kqDg˱#4A]@6'Iaۨrtt#7 @?".r<-KWlƳ1y"s9ߖ&`Ť]q 'épjێ'06\#8{@h6;+љW&>@K'ʁ$C*(]ȝ_OSvA1[e_}ދw[|wP'#A<';#4 #4l;]H gԧ#7TȠΏ]o88P">?0qPԟ߿Ct[c'H_59=Ab\(Aaz}!?~ޭ¦㌤R'BRs#42poߎ]A s(m/dSs(P#411P=O=c%h}b5h&K '>7-;ٿz! `w#4#7pvnA}t|7Ou&ϖQ ,L>*oF"z]˛EPmO\0 bRN:{~/b !Xheh#4R1fT#4f':FcĤk&8İҞ$%iF`|^>y(BE8xY#4#4Rij+v=.+Ғ-߃HrU#4sHG*em$Y)oP%*~#[+NmrF;-{^)WY=%Bجă*V=:Rier|&%s"t:²`Qȅy^tɋt NgIٸx8f;1J"<^4jolv*<6׼O$ʧ[XWUx4!|[t[bC,[3+^ #4}sDU4;ˊj$B&f>VvY7N#5ݒHʽFUU\Y1k Aqˢ]!$Fl9g<(󢷖u>l" )C:!]¯h{+f:});;gFc'&zmzQ\>aG4_K8]qY{F<Ng V50>jk_ lAߤCWJ2Z8JVޚw)UWGT5_!RTm~#70.#7;@SZ#v+@]ÛlCPc"B8LW~](Q}E颊I_R8#4pW3 GyT\w)q,k Jt#mb\V+xe!Α"P~ȵ[Ϗ&R[3ϏdE`uF5U9}7ht:KB>80ELDԙ}ό7=wOelQ߿ pƷlwZu#8<>=$loH<ҿUZG#5Uopomʴ0#8~__ѳ0ҁ;~#7-Svw"aߘ%LnPG;AF }ڜa2ʲŸJ.r1b,z|:/gr_UΟ9͆'E;+rTBҎ}ڹ4:%OXE2i1GP0"”+2q}^ #4]wN)a32$O.Μ}슟G<>mT=B*6Mr\tSO[+J#8'6JeZi)ASxK=O-YC-#7C3, r+9\9YJ),"LrGɣ9GN3\TsGLiT󻉤m_DxÅy/ dh#7vOOӿ2sB.Р%ߋ4|2ý#7N&e*#76Y UBH #8Gve40p&~@A-xwgkG J#7|y~+UߑοOZL9}ʹ6Gp%>g>L+o/Ҝ!(]X~S#SOǶA7C˶r"m*!aިdN 7l0}l5u:#4H+LB#4ͅFdA'u6#8#QNKm.ۚq|NQ%xë?ax4k ︶ÌTWEt',gvϹկZ"ez8/l!b$'Zk.!;UF#8t% H)A;#7v ?G#8ˌ#7+fa4ICWɛnӝ/L6s:gas+#8p8'H@:gթm+}|e~O{x Y :N2ӒI>O|xvvpMQڃ/#4r#7#8z>:ne&4V)'#8fX-OV.Bm2WGPΥ+ LB5'lhK\ᤸ $ $iO#4Ԃ@]$ClU#4Z>#8U?蘂Mhic#LMƏk@q#4D^Zl9=پkX8ɢo|=L,ȁhYPV+.ƕ]AtGG9UЦIQ2Byh%qI;逰Z6 \/#4c=?}̫Rx[Qbo#7d SO(1J@f)4>z= t!2n/._ k$og6קBv b=AV7u>|Q)t#8 V/>ķ\"p]7cn,n,8&[5 ?SU}[-d>ͷL3#7@~e"bOX4Y{x}/~i$w=Y:hF}O]߄z{O0F$[E-Mi%Sq#4|@9"7|t\N uڶTd[3ZV ʪH_zH^g勊sT !ڼoPT)=)ALCVxDD[B[}ƈ#4)$<=WۧVFsgڽ{2?*˰ mcA꿞@>-7ɜNLThr *Čadc5l1]H3sD5K?87M}0kc҉ǫhd6NWU~(ԯ}}qN}wmGzrL5g#sk-BjEL/PKMѮ-i9ʆ萛tk"lbCo@ieo=نSq\%Nq=ӑlq<2DP(b:YON\qC0;(*N9lg#/(l@÷-u#4~OhvG<:M<OksYupn@2瞷9;]1#JD#8Ufd0C\~>@ Q#8EyGU#4&zT<L|w-BHCTaX7#4#4G;Ve{vƫC1Ux_=]bC0W<b!0)z(0mم&N6,âhb#F_RQ;P=B`@qJYxH=\hag޺!s nTh<9)g\Hct CT{qUl|S7ll4!LiB۾7~" heѥw}0-8wYN}]d#7yokHmim.\3;]6_N[av:ŷdmd[gY wQAcY;V/q௴ăBbkH* Owh::J1k]Xʃ0]h{'(݋a'9J`j >GuWqMPE9%3ptVqr|MZm8!vzQ6m}tNPC, #77w7#4R$ضCԋȎؚ_#8"Yv{[^ s CF62~hf-}H7{ib\j%?6PtcaFN]Z|!!#8ޏrh\e|J5D(жk#7#G0ZjDb TS'C{0Qzi5J9֜ #8uɚ];[ 8SqpEG(tI}0:Ƴe#jw"}\0l;i -@K#81b`BP_Q}TD,w\ # lJZN2( ƸCqQ`8kF͎hiѰ-1@1P[(TFAuEQ{@x^R+b4wDr Ȑx q:/EBEx$ڨ<#8<8,[ l#8#8[-q]]VoOĶ* hY'HWyp8o>8q#4bY^(5_ݫ~/#;~7/?>#8P+-#7xYj!A!֨0/5BȈvPLĜv>r.Ǽpx@ak.O+vLo`}W#8\2f ?Խ7wr/s8co>|VQ?ΜL#828~s;FV;~̸~P>~/pFm#4{J€BOڇv2#4sqE@'#8ԊV._ g$"*Oj?]K#8 Us}4&:F(jAX#4? Hlk?}ݴhW #773s\uئˆ: NԸG1+ڜ*my ,Jd\vu{Wl#7c2su(ل`K]@^A詴 #4;;OOxvQ~Dpk r;b5:C!X >dMesa$ wm2ffpS265#7~÷3OXb~q@(#7˖1uan0n@0儊BsRhp0`"8y#4lam#7X:B9W? ߾n tFXoWR螝#84ٝLjZce^GtgW@l#7>XWL_ ɉ?3Fq'c~`*apzQh,B6PàQ>P4sf/܊wEW0iE؄3#7ס0%'=ESRqd m@$?.?_p%GD:c?vxXmlOAdĞ#7$ڝg}%+ Oʯ}=#7r/y(2dn1v_|{!GX,d.DB#4funJ8#7oE?AA4IGj%,{ IW2O|=nǘuZj⾏-$a빥!LYN^Eʨ6+[!x f`r\tc%nZ-#8$HXӽ+Du9&8=f|^g6a37D9?zj&@9vAm;Ͳq%E6~`|'y+?v]Gz9!R0~-AP$#TO,gvo%Ur/P.T'i<7@J;:Q462ZG -R$<n`nh!1M(U!  &QYsϤTZZ<"@ Cs`sx|hD416>#8#7?7}vxH4RILoOh]=3EKQ!@RA#4tynt-I2UpT?\2Y#7sUC?F[fKdt2<8#8Q&Em)$pà8{UioE8<|]Wx!9}cTJ`T!vŶOm7C@e&PtRw`O{eXwitͻ_!2d d $G%lS<,?R@ ;VUTɟLpB>aaP~V=zݨ7ԺReݻd#4mٹ/MCE+#k4k4BH6C }^=8fy6YU^I#s5QIuakm4ilV7002F]CX@=S5̆3oxf [ԖէQ!tXJL 0NgHHAZ8% ș(b r:#7;+Cھl34PlJZp!#˼I~m(_@y;DyCЖPRɨtTXK<ǖ__6h毐^m-}хy3:#8eH!Ke&&I3B[\$M #8ݝ DĢ|4rTֲZMq˦{/ɧN{N褠3w:wdnm+.^%8 Xc)վ߼x1I$Iai(ߚ3yMҀwv4&)^E~##4QE2<<}!l ܬ[4S#4tŇ8PDs#4q~+E~~==?OPa?WI!$E`K{Q#4)nQGCs0]zkw#4#4rlvBD"HnG&sB,iϭ"i#4=8pOyC< D]o|e2J6E)#4@p/=Iхla=#8sHӀwd!;X_3c$U$Es ed*dŢQjs3r1Vy} 'hb#J,d&WhȇIݺX,Itwv5P-{}| TF2 "!g^}:;#73-WWZp}"/dIY$dczTJ,Iv"}q4bJhyדw+{yj03#jE}~*y חǭ*qcrjZ/o`aQU_˅.Wy^?֊xӅ=N@{T1x?|;[|SN(?myP6=UmR9@\GPѽy*{l4`/Qǁ!+}_c2PV9z;N~fHd g&"Zw9 鱇z^nF=6<9=,{˖ ,z#8E33+@֋j(qy'uWOq€<k`ɣyɃ7.WzzFF%LV#4I4&p-.!0C:d* ti=R+,%\ &$$LF*J*0HnPL?N@(r \ %a"4=^/S:MKX1CpHSf=A=ħ~U6:(l,IdIAYG~uV戤>)*H-}(}EE,d(ѢS%[JFֻ!fEeWuvZ0pNa{>#49A,B+ř/"+^Cc#h'>'Wxq~xnN$,A1TH `fOH(d*Q/.-8JxwudZ=w|<(a#7b;#7tnOIʸg1'HL(S@D/qFj7 98#8 Mo{@8OXNy *VHAlJ/t_cAxGIH-݂9Q$"%QlFe wS'3ء7).ksOY!LWiImc>;Q=`ʥ0*tsV!\a)Ԅ#7eq{LE\_pKsc;ωn%֟Kg"ydB_FfhӀmK0|՗T9;Q:(FiiW ǐv@<]o{بm:Mƕ̬s ("P'=N.dBPxd.dH%VeU-Ëó˔tu],'徨7䴞KONS8G#`{D eph@ߤg(O=s#8}!>$P7>=~#7\T=]X"p~6(ϵNi96#7L/?8n딸,589?ev:ߟ{ޚw#7v A'sCP#8 ZY#7vaj ۦ B|ve!2lA.gpv"aSR@˩Cz+!.;#4㴏Ru;?#8#4ve'VZ#8{qi=+na#7*K^'Cȫ詒AM#4[q[ nh7@xd g &Y7#7p9#7c#8f \c"C`Q7/ 2(XDQFOu(-(3*"5>F#~8+ R79{ÝxrMm#4cGMaZ\"sbgc44_Uk1TXƢe#7?] Kwu 8{?E3B BtPN>3'i?jݾߙ}aB%͙Fg%v[Mҷ׆Tia:e;(>#4'ٹCc].ܯ O_3{̂BwX6udR$`: B'(j[w3-G,??vASu#4k.hFa-g,uWKI]3- Clm*RPdhl!"#8K(J#8)z{4kOB;TD&Ɇf&Ȳ؜ТXq_ـl c7/p bzY#4X,-aeЛ~k[M{j]b&c|fЫdG@lêᄈ}t2.g[nY#49s/8[WƜo6{Mt'&jȚa7k;];+mq:_F9Zy@2Фdff{dhu2g ?eҌp&ޠ)C*.ҰE] !8E-[v+~OmF2Pi!pWiR?2cgc~]Grfz#Qm= pN_)"gdʱ:%,#4P$#4C20$ߠr Aj2D8ՌW?~v϶HAiPJpUDb#٦0Pz Hsɜ}|5@pn/'Kiҧ^MdIGt6&7Ȇt*6o/[g1R@}?p{7A^Iu_cpo#7#4Z]_ؘlTmbjy's#7YAp[}MBD}A(MLrz7+NI`rStA{2ƃRι#^ȇj3vqMw[#8N;^ONt{ 8Jy'tT-0>)|N7v@e҅/%r'Kw}E`Nj@OUý_7߃yEE4y&JSh!)0L#8s/Fy;.,v9#8,j{(*]{anf]]BOˠ,:pc!{X#7+t̸4v|~ܬE#8?W_+Ӎ,m"͙o˱*B8U"#7 gϜNQCHK## "{/l~J:8\Bw(__G!l͕4!hFo WS%5:VS?)>Z[WBJ|=6?)#7BQ1" ]Er5ku+@k}s-JK#7՛#4㖕LoTjד*u[NHEcCLF?[mK.V1"eڇc%{Yap[J=>./v5&E*kttfT0!.rU*0|6Ěo,_n8x[N؂ -OZsj[ww<Q2ڛ10FΓ{O{+<%J14T,HmAC$`"cX{k)7"A(GBޠX}F,.G涐~{Wi00@C|AwǏtujt#:sCJRNԯu#_3fHgƘ_hԓDIE1|뇧ZFzò_PqEχX"]ǿ"BM!1%#>x3&J$E?ML}UB#ANgd{yzk&z$@u,ƽ胳:}?bu·w;"/\o {mOrN@f\#8Q<Ǩ+ _\z6uVZ0#7}t!;Q%P?m~3E KH#8P#4>op#1ӔΊaAx""TNܔ®x[n۾y>/4!;:%r&fArKEdT[;z2=r4zLS#4&js'm E5d_f K\4&.(*omKq>O s^]ťb{bل G;Vϯ|>I"5eR?0ʎcEFSo#{ %#C_T\Ր;&K#40Vcؽ#4/;ZR\D@C@G`?폥-Y-(<<ەhITbU:_=>Ö_UE_?_Qd@͔0|~s?`I>&QDo0AimUV?9 ALŗ2!np <7=l#7w <;Œ8ar8,F!%*F(Dcdz]׬):vW:,u:  \ kV]x yyIwBw^O~C`,K}!% Pc{x |ΛnWzr*%K<\>@U(%fǰ,f 33ssP/Hc;nrH7f@4Q@Ypy08v=Pܢ%1b\PyXkfX&+1+@Ϣ)T"jH]|x; 0ÁA k 4##8Q3K@Ep0v+ f"%{QiM:˛tH#k#4NxUZCT{m9P:XOg8#7p^/,(^#4fPRV96v۰[x0)]`hܕߢ<`IIyx)i;7b9eXAM﷖=WU#8PdCqd50'SaDz>7kV/9g[b.;!E7-$:0 xDzw߲=f"&8"6Ysm*S)}8g4d֠ohrZ庶+f#4ta c*Ө\C#7`]:7vC2{| /&<;7)RuMaR'ԭAc!raaSj3g`Სoۨg߹Ylt{V NI#Z~\w+q`Beyt|wkזWxQG+#4Sz#45#7E AE|R &D.3֝7u*R-&pa‚B] O(Eb(A"ƺ 9 pHk/:!6[ km8K6f3r\UN*j}O?@XE6^K#ɣ0ߥSxca sIۨXjgi>JhwN篝:xI韱|a/fg 'dw,LP潘Gqfv BS!\${Y~ C-.#4ӬاSO>r#4rxG⪂EcpJQ88MhM %a޵wKu;̦#ȡ,I(oXĤ6,xz#7q2,'Ypygcүg|iE@ $0d&fީK'cu>kJhR4Ui^ft:.3}0uLeWfd˙clƲ3$cgٞ~hlA#7v7*P"q#7b~VW<܋O-#7[ʸi HcRtD>{xQ>quFx'[vuueИLp}9Ĺݙ{ub)GVDL'/X( TPXTQ.圴]zNh+wdj{XBdj \Bř$%摍ll1K+:6"dL]]x==arHv0T hhTHt"+A8@' hj;˹*ƠjBPjndUC#8<.#%UG h,o,fA}ƕ0+n;#7NDDAm~R>7kÙ㍃u#7]Āt=:69lوBSUQ=4{jyhM#84! &mIUt3Npn#mh\(A:dv !5F-1 Eʌ6*ie(Ta8 ͙}2#4#75",GJOi|sAt6B,Ά%@"iDSBqMt()|"m4I#7qɁp#7fMQN#s i#4̇>#8F#7&2#7ɃI!̴ֵz)o3ZTUD&⓻U؎a`6#¯WwE:J͢f F2#C!ԄC"zNubxdbn|u΅?gJ::h {:;܋96v27AdvРIpa\(Cr~{J7 1q~L dCwoV#xM0;&bjP;u>bMLa [঱1(%/rMŕdX z`l>#4܄G#4ű!!#<6°#Di+')Ʊuȏ{7-~kDM_dq{vӲ]*x$/O'R+f 8\ʈ%t,}ԃR%C2**Y7Syd޷ X +IHv`&o9CeuQ,Tt[ԧz#4Fp!UJJp*BŠ#4^S˽X#70*B){?CUK0 mFJK]ujۿjѭ2HP UxzG{ @7Dyv֋L߷؇J[LR4R~yD"F1H"t;̒R@i 1}x`l@nr44I]UbجR쏂*݃I Cby{r=6T&TQhD=ԢH|ΖUvsB:8dTJNDO'cH]#4tW{H!!A}s.PRD)DT9#4f@` 7BH!!aA@5̉Yt#4Z=!J+Dzrku^b1"K) Fч#4U[9YQ0]2=#7NQTa! 3%X*fʌRP#}Ț_TR>ݵM־܎z#78HE0Á@#4"I')m6j-J>w+ 2((H*Dr" b#7?0G#7𨩬"d>4ChqjI KVlUF@0gDL!F!q$ nb##8V CC "[^$Ž]zm^zLnŹ*.nwrnTݧچ25Q6$:m㕷|͔ &a$B?$Ax!2BJ;͎'"#8Ǵ=l9ׅ}Sz~wrs d(g)l+ψw,_1/H^΃H2 `[]sFJ )#8EL6SZmUo[ Bl !1 SV?#4įH?՚z+,z3q<\]aw$ fĠSydYJRQ o#8iaIq bbD2쁴wMd~Kah7ÊR7!6o#7OyfՇ%_}Z]|\iTYaC銂ݛ|6VB-5NZrjۈ4cwU {piJA5LCl-@1G'aFhyz~`=4]ZuiC1.P$6ZiϪhS"Z Cr@rcF#7A U͂SS>`AX=5 ۰:AoqdРg֭5ʡ'TI~g`DA1h]ҙ'Wk֝v~x Wt u(24i͠rؒFu{z߇#4Go(_una(.~NX(ip8V7p%X"O ͼ$\<^%ݧ˧ǵ*# PoA XMBSI#&YzܬB?Y0oc'pkXRN4̊sE(T_{Yqv LNboCFrEci7B#4)#4 0|#8JX'#49'Ɣ@So_f#4[X"\PҐՔQ@ҍD`Sz rbȉZJ4LI3tKZ#4+ךv8-v28C8r@F^n!5}-,0hU >}.cD8 }t1P4ЪY1N`VA|ھRsY5@&Cڢx!B!v>H:qi.#4 #7raj?˂e"Qk Hh3{f+J=Ԅ `lA0-xgڸ ydO\=ĭ{cKEmR#4Re5J |]C*Xfa.ORz#DdFS1}gu<גݴM0N4D0eE#8,+|5}ӫLX̅#8[$[q-;8< mӉ͹~V%`#8:&:D#8_ϑ_7(|T L%mzwQ=fm"<#7y$N|$ߝl՛ef77;2{v" 6y_'W&ڠ~mK=ϱWuuSems43d\Guu6r.K;kZr5B1\b+U8`%DjDlaI$#7E` " :C#TI RjR6ٔVVfv#9yF˵1gkϖk8Vw_ooɺ%>F%UAJWu֔:X([R$hx?Ar;><#4;4蠰!;ki?$ 6v2Xs[)pT(T3@u2 6~#ks=@S #4n؎ap* 2'ʑ]M1=i-Ǟ< Wgݿ:/~hx7uiES%D@Q Hnh" BJ0}Gf#4>$KlD(t: jI<(1r$DGyH}_㣏d\HLw-MW'S˅ h/Nȇبa4"e^Dx7Ywb $JXѶ.SUp$U &#uQ畬Cz maBBN%0< PZ0B5<3X]˜[!#`7g[-!c1P#7T{٭3aWvr#(O" D2JZM>nQ_MS3|:Fҫ-Fo/#7}9?Nsc uuHWfx}x! wBTK)s{pw4d\(>[W.H6~ : X#:Xd$<(նel\2u(mb˛ّa쀂᠄]e~!!i]U9)p.pF:P$9lS5F* #8: Lv$ d mvW#8qдb{Oh*B "EЬ6 d&}M4'bD:A #H0BşYyE0&pY @#4KRS%"DBdL&0b]jB/˜僞;||S U&bESCJ٩S24bki#*&ؗΏQW[&'0"3}ѐ |\FD5R,èP{\\RFTtq۰|Ӏ?8;o"a@F-vąS[?Aw@|yM%k"BBbeؕ!rgNX-&I#8FĀF jZJ|G۵K@g*YNE燬߈4{zJBT'6d3NziVSAn"qJBtB uckry%cp YYRLdH#Bj^djiEFy(& :a#4I7J0/YZY3d@8m4_`FDǸ^MDE2;8Rl%zrRB?#4;) +V9!PDl!L7M_(ml6Qǧ, ߓhhDm#8`5@POPBOCơl|RYtؒaT1؆j+42"1f,1GI\jf> Dż"*P,><8t*|w;TF"J-#7ɠɣDNnfWX:]CЬ,馬381ҡ̜sOmBþSnKS(fi]NKOQutl?RxD#7zօ*6 ),#7>:=XSaD4 @Lg$ި#84$*j#4&4t$seX 0qk8M5"\B1XB0»sn8Obo,]ZCق;=]Y//zLFo$3f6z mG?r96L[U)f%Db~ӴS®jJpbˊ)_AHB`|gOuO9}%m~UCH6mxmڹ[sm vFK8E5bGdx*R)Cf_nAۜ+yI$oC~rTdtm8`de(U` 1imSY6h #8^ν&f,'PJ~c7aAf!tgh!B\(A ·x.0OWZ|ߞB!vE@sg6rՆ<Ƿ!R VH3uU:8ѭhxQGl_lg#7J_B$ÚywU|$n+U'+)_t}SX49UT[iZ?l+[;5ѨT"樶7#4yE8B ZU_ZoĹoA}&\H҂>y3_,NCH1n[B}&YýXCr['q=y$- l6 *T`sXᮖmwrZ0d;x}KPxQu5DAF09i"gU:@=lg؏:vx?Gk`z7pKÚ9C;ի Z NI֕R' w#4GӉ⻓xPtI~ZCG߾;{B{_ξNQ((!aR˙9 mT_Nǀ@+Q%>{2f#7u5Uu!٧4W^,#7XH)Ȯ*>JuxQp2t(x|ZԦ&ޝI|#5Ʒ#yYx5dJ Q̋`~\fa|f,nj7N9yikcfg##вZbq4hQSRZUmozGy:Y]l:+4vRq 1hKqZf[м?lѷ(0E;<.#7޳1s+R!BH_ʦg6w}NFu%dXځSܼiwֳfbjuY & ̴DJg f~]f1z#7 'Dª8Z#͔6Ϯvb#8ݵgeM`,сĐ#83zR0gF]Xes4#7eLH:ri(˼Y6k+uw0xvۇ5QK> : 0+rkaK[˿6ZIQgn3zb[<6n!#83HMG^'Snz[QfGc:ƞ1&ӊ-ZWѬ^+y6@ܧ:ӺEU>k|`fyiy'}nl q5[Dl3&E>6jǙ#7٫/xhf#4^\3d`Ȗqd1#7ʂ+(‹lc0iSYqm/Y&Թwrsr)+7ѲOה%Id 7s%}S.7F'`p׺NԶflh1 bRo )f(L)kX"cliɗ۔BB9F鈇8ESšhި!j HA ̧BȜ񐐹L;!SU*YT;'v#4r2"w8(S_g-KgTlm兖*$Xhvlbc.VϵCjE4&E ım’Fȡjp/9dmꬼ4 )4ua#7e8a2ñ|eGOLZ6;EFE$<#7fV2`ͩ+0Q\[wRLwdqe|HÁ(I @3s96 ['#8#(r PIf0 *K Zº1' 5+h&DBD q#hif<ֵq)kAz;rM/WI#1^%uBI3 x#bmE:(Jx΢3Tplj^baQvBSq݃Nim<(r2lya ځ`:!sTej' #4n07IўL85F-s^+ⷈ,fl hNMzbC#7\In &SS4,&!hͪt5Ĕ#)8bɦDBիRRC#8*[&KH¸glm(`RqCW8&#7P,4@!!͘J\i3 ),nBӡ'W)x:4AP L8i;8u)&V"&bbTAd2t)"Uv[l-cL3ロE6r-b(w44I!BQRZiY&l5@]lTtQ Cp#8A3KVLv0"8mZrw-SAWɄ˞TiYᕠ)#7/w63cx] Cɦx#7\ fjr\v M!L"R݅6H$Y)004rZ 2nX#7 45ބYUHlimD+R'#82R+ǝm:fDY"arq1`#7.T3g̅鈫52۱6a$mfV8XYfL.`Ca#7fjaulJDCsX1¨H7ܐ8 Xrb;eT0n P `"i4N=Y&q0ǙZjYG@}& KnxG6QE]P2㥤H#rn3\#QYt.ݹɪPƓGIq.:vMv٩2pna`l8P#4f{ykJ3FLگUk, )$B(bf5(h…#Jg0ӶCp*)duƍsyt5SWhwXg5T`#td4:FH[K6"Y,#8@D2!&a, Y!@QdA10۔dJi'ƝUI>#7l^d^p9g\Ru]2&'gn&!Kq1Ն#-D#'Z^릴Go,6k[mgvYKCn8wKgB`GVm#4c=r[SJ?oC!"H9! (hr4FQ$b6 B*Bj@Y"ab:B0ЇMܥޠJ9#8(ȥ2Rv*B#4eGohb!C3wf\tݷU,#8dD?ʨBoq=P<]c%G1v.Gz'#48TbLo$<@9} ̋VaO`~0ݐϤKI#4]6W߃o9w2"L2F 0d@R肛vĔo@P)pZ5ݥD%-R#.VЎeA2xwNEH$l'M!T`7" DQ^[΄AREQpPA:EQhfE,SlʊA7(A&P #8ؼu0#8g$1Xr#z@(=wMnHH10#40iSAА.B(f&%h?,#4)N"S2N3wIq#ClKj8c}".'rh8QsV=vɜNH9}ѶΜie[stQEz#7Me|M#7maŰ8#8+(t9l4*D#8_ ;>a!9sp6p+7c[7:#8CQN'+ -a#8 A!t8f:}tI1ЋЯQBUla) frQDd*3eܹ hrPjpIZwijTƣ^č(':w{!K[<*gxn(=,,#7 F%ȅ1#8svaf$0@A:#4(Jh %D`J#7LL:I8@-P,\63YEpDBR#0T/pQ jSj &!DdHT݇=!S#4":S^+jEU#V0 ',$#4#8 Hy(A"$$=o$#_M!fO!~ŗ'A?VrRo4oXQmVV|OF˺ä(wGtS/&ZR,N\9sI)C4ogUSA46kM$iLih1H*]/j(*@΢ԻSSmjgP#8M^#8ٝ0>0hͩ̐ *FYYn7DAﰞ!ÛZmӄ>ݦ1g#4$ tA#7K#;j*gݴDoM &K#{sSisg-609;k;uk"+P]+u1rD=^Bfdزٰ9l;>#78nd aQtܡ!KT H,1XS0 Q_s-$,KJJC$ZRBCVDi$8(I$Y,#8T!" I(7XP[d?=`ze#8BDfIWhrMwWyJՌT"Aw0m.jnݻ&kr*TLY[[-%YnJD0#f -H .ۙMWnTRdS5-tM%R6[T>עj5hcjmP{}%WGbݲܾ"Ԗ f#4*'hRJ"P;!>,Kƨd"@Dˎ7^'T 7A/,b*Ɉ0ْy!@ vKQZCi5u()TI)$"VdƷZM6FRMljDSLYfJVP)F~5Uf&f4ĕUmSDȶPӴW] m-Jjmf6_WJ^dQj[m$mܻihRMJ-_.vidw^1ОfI0:|Qj">@ iD-6eMEk[{hJ{LԴYJVlK}#7ᵁ#7R׺ڮj٬h^6yMWJ*QDJJd"a"S`w9)MEUYőK1SE#8Qid)֛fɶҚ)R|Vd(lMJ,I#84)6SH4l4e؋SccIjR,YJ)LKIڍXY#8JJ4&Ra)iRʳlFi+FR2Rdɢ JmbB U*$H,!SeJH *X.ƬUX)#75$"D 2"}EIc^zC?Ż7\#Qb\ [qo3=uǖ 0{'~y)D{"%6O+ԩ0y yBo>ʖxk|#4n@艿Ӎ'#{Ph]Xt׎T9٠̾e+N?򞵥ߙإ<*ROۡ>(7x71>:_~9=ݙC(&B,Zh7w6#ls365B1TA(A~`so(-#4n'p >IvF;&ʁ()_)A=[tnPH܁ /T\ Llȱ0=hפKt 3M*}/zv aClg2-'G+тTJ`F?4_BW3Rg#8Q,X!͞vsYm,5ކN.I=mEY*@(uk߳ZQXSQ#76#4ӒpY #7&>x2EE uwC ;(qq]\RJI  @#4#4lj][SIP+ģXA,%h>,uF/#7/&4lbFxʪV@x֧ccTf8l3}=`R`kj'"#8\opDՠs.<Caޗht;j7z(M@C,eꝰ$#41?Ql#8.[s(QAaINy{;r\SW@"Ckk{C+) *4hiRb4C D6&C"[ᄑ`41cXnRqmj+$dFr*1ijlL&T647&:W^x⻺;cKJ)TR⑿J#7PT#4Ӆꠣ'@4/(Pq#7'PN0:w:#7(3Nt④T`SHҦ\+ۖe#W@a0&xU}z !T5J n~mU^"|av"#7iQ@|R@DyA~]Z$1,ET4B0+kH:ގ>Cm{`v2b)M\jK6CJl8$VB f卑 wr#7ȃA5}nQȞ޻]#8'>;׸k.V(&Rhl) *1-}<.wH0@H! it,%Udުex9x)է"H"%'G+P&9dh~؉[;M{bҝXtlV r-oy!31EPmpTPq:n]`Ԑ8p#8eh N# G-GS UD=9np.i$Di5-e%g&}ey8+C2ZYES`84iZ*4 #8Cb28ۓG"YDW#4jo!qRx`E`ԑr?W`4̞LS'NLZԢ])،3TJ6l"t:rH"hOi@7 w37 )6hwQ\p15aizMȍqufCާ@vnHE]=e(C7ϭo+fy*wЙF|1E{b H1P@D]9U{hD'j[l,-&B9FCy.c(#4HցcR\T pb1nl|/;Oo:ᗷv3{WrcywwX9*#7欹GcRheUEEU#7%Y6tК`\+M>$g$r^;#7)kLqR$`[F.eİ$P #7#7 g]ꢈ*Xp2[Y̬2D%Bw%fReoϟ=}(NDu#8N35P pC$/R4лKؘ q\'n֍T 1|WO#4=@-xbB;A'#v3FzEn!0_f%ͳy=K-m*$RFo`rw0K<{b2#Wˁ/#48Lڅ2H `*BBw5Wi)+k]Z[m;O{T.&ZȆmSH*iF"PRgem2!^G|WaLxtzLn:#H$uǤ/3jwX}}Bi)ɫMxº WRmaU_Q&Y8#8P:U2h3q#4P0h%(MW(KKusWLIlV5~jʔZޗZ/S5RmVjb.p&J C2ek4,% .$#4@eLgNz7i!>56zpiz!pga1!8FV.J[M!B|8s#7'=Ka#8z'vU>PmAp׊#7wI8dTJQւXUq"2e=v^crPz_PE޸@VfZփhmʶ **)" 1/ \VQXz_t1 -a"M& -uHwI}v=j?~X#4t$MOrQ&sX8tpPXAdP[-~ʯ+ҮCv_>KRgń\qDYHa 2G.V)'mnJ-ˁG(#4Q;kQR(H,hV FM01@**Q3 ANRd`S)` 0?d0 . k|aW[+(R@S{rIU `/xUh尫27fkz B"QL#,_#8!bF#48XPDD@}lhG.a`\Չ(7!"H+,-⼥j^v즥|VkoMyȊ-Ս*B6צ QB#8B)]iI&ڍZ!1#42Y(UTP$z#:-JiC"†[0lZTJm5#8\ B$L$Kqq1#[2#*W$#4&l3H%UYG+2%I{$ّybipbэ%U@qߏk,,J Yc&#8j(0}P YU]_>'`U%u7=π!-f)g:ݷqIYiJZZ#7ƚYn," #7>O."yM@wW#4bZwƭ8hZ\VD/y$C}NˬQ~M-yWCFicXܱkjU2*/c%, Dv-\d"0K-aaa@ĎƢ"oM#7$}Ixi{< @$D4G#4#0@mFec#4PT sW摑C|+MMEZMHB /|MGMN#8>Spkn HěG6Z#gF4C#4Yӂ907ѦùPB#4TϜi]#84 `L1aB؉ IrwlV-QD"!mk=Z6 G".a6e&.rD{bbQ"-DTBALZZ۳x5yF#7}HA'"vYDN #7}|nj趹_½!$lW(ۺ19yw_\mzȚ ".;#GoL@"OOk HDR}#7bBD#4Z Q*/Q/j* tMa?, #7KZ-<["h#7#=g) ;FQ7!#4%SgVO9=@ PSnJxR/x!8wL|j0Y6JE#,KP*K|0\h$)F"0vFT@!#7Cqh#8 2PJBPC"gAZM3qKT#8 أ4>)Uhfne#73>@ih\wt|c*ZmT6n4["Ka'٬#4C >V;>w"ik}O95/ 'ًX!vi(2ь*++ݏDyUҡBM~#8l25MI@ЎQ38:i:9ΗT&:?mVbf9-A>JDIȔ{}zlb*=@ZaT59@8"wً:_N Sc]P+#=ޭn-멥M .Se'C7֙=#4@;#4w\P:{r8q=S2݄#8L7|ݬ28w{(2( qxQKp)ى}+hPIV{[E:@m#7wq;̚ɜME?/sɞIze1${u^06DjLW^y[vhU6KUXj+PSHQ#74@P#DD*h!b#8 =$ \#47R/@[EUTA"/ª-ҊFF#7`u2 hE" +~#8mu+\3Mkksx]闍Fs*W#4I-1xsv+HnHmbH #8L$b   S^FCp+(9ei% "Qd|#7`TYujY2IYl#8`EFЧ=.B4KRy1 !b#ن{}%;NJgj,#h :ӟ˲5)N]>ʶx;PV%t#8l^p&^sy'_hhJ.$H耽KfIeEdՔشlj~e_n.S|<TQdU-,T~< c\4\ڦ'#8#8#4Ck5d*s)N]g>\ W($a2==z9jOq#86cz!rlp=s=K5L$䊍2|;}Ye`8m#4䠴A4$nyYRb]q\ ",L6uØŁbVNMcɒ`kB&F#T~jM$G]mH#7/lxQrZ'1v;5:8Rs^_IqkE(o**A})Z0l==n@B!F(%+Jn:CO4μ{Κ'U.MhfA}Ig4ߥ #8G+GJfA@1gxP!5} A }yFU*[ o{X$A]{1#;DhA-6H`OU$+M &P* )--^`#7o~[ p 'tO_ bF05U[X]A>0D+5VB6 ؤpR8`A*0  fKA0@j(0C"Cb-(nu<Ӻ;rp'XuTT8#7|ioKe$+@>[oUo#Mciְl:[ќ11/AfaRZu>{_I#7(<!;8ޓI.2Ytfv[l(R̤F\K01#7#R=v #7)8G1Da0gzmD] R`Q gC Ƶ)`].A7*/^snjz#8IP: eRXX\4+D '`Jl 5} `vN3x` ,0Qp#7xo=/Wn05@a#`0TPW8|v-2~vT;O.-VDL%&8=d(ᣬ3<#8UQhux3k8M-#43l0B%vM8.=': #44bIf*OaxCwo^&U2bUEfQ<ͮmsr7wiU|+߫OFjNor(H#7:‚U5 ")Slm#7uQ$,E|4 (01#7Pbaci7Qq(GHdq}D$[A$HD)C{dQY01K\낉dX?E`|L^ xJ9"#7kңxknKjMkv,U/&nmݺd9xZמi-IEK/S`c淬Yes|Px#:sjT7#4AP@QR j덦eX+޺*MonIfMz΢6ӻ)1QImr֔iezYъMUHf;NĘCQS܈P2r,Y pH#yeu]m$ PU Atؔ#7uC`SyQpw7ųg?JJI|;e|yк(#7 8ec0uC*1d@]D63n0ojn~,qew2M5 0V ښn 42G;rtrÍHBH?isx{BMzxLl)@\6|U90NAq]%}ƌ LY/6Æo0sۓkN|ZxfhGkZ*EaV 0e,i{ X*v+c<"ˑ~za>SBZl'(Mv6C*dž*CbS+J3xg'bJp'((0(, AmjR›{U{|y'RZ@"0(BրI{5061˥?V$E[_AC/+ BH ŠiHVF1F!h"X!(a(As.,2T`2|] 0VB(+$!Mm㽲ogu=xw}=t^ "ĂvqkݬU)1a bfٛUX#8(chPaBTBB#8]h0@Z!KQ R j7@xO mܢ^Q"1D`zȨ@(ӓ{j#86?f|e?3֡9ιo"2qeĜm>G7ۈE cq-Ftφ΀PVFߎ`UC  /<>D N¦/@d7CMPTIC8b9פ#77JҞbU} Dy?YvoMXj6N8&bCD6]zuş#4 UuިX(#ŷ0ئ#7 Ј%Xikf Maɝy<16W($F #7UpQ#7!Fy' J `L(ѴmPȰz?2"^ ~Tȸs+g|w)·qljh SyPRM\l~C㶠 #v[[ZꥺCPݤPR\;$B#8Y#4t5N4ign;ma3YZI:&)dX!.pҎ^BJQ͛T6ijOnlXh06snln^xɸG:+HsUYo'n]dۼMZ2<]6lSfY,65wft$SQʻeβѣTZۑ"4AZ()Uw6DKOT\#HޮP9Ai؇#4_xxB#4dE j"4s,#7//SN (hD9Ox*w YH7P#49 b +07(ljHY0Qa _D@X2V!&a>WUi˶1nS˻fFkԵ⼖oQi7 aĉVz}#7ƪK;!H̄eK{5|Y-oU^y;nޚ4z7-(#86D\("9P\5;'o1;3 )AM'G܌Ô.ҡ0bJ!?wjؙޠ#4@$#4`"!(E #8;}Fh:=GEX1 XHH1A#4u&1؆o__&ML)w6HD*0\P4S:!_\Q9nP_#4Bm#7J=^w#7C=}#7V>#8&=\ax`ŢU؈cMB$s61|dG[`FRIJpP8_{A| ↼-aUSh`d%eIK?pHP#7"MJ$uua.@um,*24m)3#4X%BTE+F#7f+M#7LarаE#4,%+#8%#8؈J~*IoEL(*$^:E1TL#78: R<ׯV1mhZdzZ<'W5S]yzd$ IT)RaCf3WaI~h\ ƎTQQ!,p<#8;/MtCt(Vf۴UBADJZn5OQ]@Caϥ@ @ꒊU#4A`ĀQ*@v"Sm[Q_|S,ޭ9`-@[0lAi;f54{AE?^}ڍ%@MC8"8n#8&d#4)کV}b@x8w԰HynOuڇwP&GzvJI#7!`?rO>qPCr2~A~c|E*c}UhHLz.c0"ll3#M9cPAQ[HV l#7BƧjv\Aƻ+ۋU#8i^G_C5i%C異|&Dh[URgppZɍ2"ZB07zw&GHF#d FrxwT2D*$s/j81c{N[Z*9&ap\*WˈwO_~if6JZ\[s5q^_#8ad#8 `(もajrY V11T\H !RzVCFѻh#7i4bK ;3쾎n{1ӥ#8q#8B̓73aZLA=o}a#7l;( {Z7;S{kã-r<9ϻhJ@`FvM(ˌ#0:#rfz,h6;}I\lz)HN~y4tn)r%ŕb!)XLp #8yiy5S+Jf0gl阌 s#v<|#4N.YW/fm/k<ͩ`zRPDDmJ[~Wp#7;b_b谙W:#7|#PI#74[c#7#sϞMm+ir\7Stxk,>HL5ʪJM(#7O J>ظj#Emm\>הڮV\Ǹŕ_{B"dax85[#7X;ax< ;IMvG(0GWHNb@Ȁq+^6FQGLgD D-jA.#4B2tVkP`F*OHVaÒ&#rKMa>RhY!; wgx)zf$jf="./2l9:qT61!+ #7}[0Jg?_\pqwe#8^,!3˙#4D#7]0c1#8U>i@Cq~iv H#""x!>{|F#7)Uj(Fwh<!^^ԙ IixlC}XCa@k GK[p.6>?Eð;˸0/'*VPVieS g#7<5we#86쯟$||9(!;s{A#8=MkJ>t:eY>mPR&732TSDDaT $#7mrֱ:4Q$:7L|h #4g\r9\޾]?ee^~0X -Ҡ`>/4U"#7G P`$wwfL~Nݱ0ԣYxyp:P4lZԑBCI#ltg#7KuQ{Ɍ^ӿ,ߕFKRQe#U[9?uTEcmYZ$B#4I#.|l;kx+#4@:.k/ ~}̌2!MY$P_om?kqUFeRI15IӗLhb#M5#%B&͓a#0Ә:Ņ0RqSߎiI&ͻfPncl4e 04hVMb#4nRn\))ӭC<>S2"#7dUabR~g'LaCI6"gcvFfuIh0Z#8|glj 60l[QUSL7saS&Ѽ'Ψ#7v16Pf8.Ơ#4A?uAզ2RΔ7o5I!&_VwiĊ&9J H-eV%S*m\k7Mdm^wށ46QH&!#4l19)\ݮsuv*yFi$B#8X4Qr!mY!@—!|R "x4AxQTm8bL)W'C`b46m˥;[mƽkV6̨2IFQ `l#ÀfM4oI?tj2ᄄ4fao^8L%,S4`7a, P9)TqQmq#7:#m6EXf(pHI*OSoЬ8\)LPb^HDHtO`&P@08~Jm*!hC#7p-l q#8X3*ܢ͔$J +*Z:UJ\Ʉm\m#8aqhMJ2q*²ZcrMƛ)CeKy5BՏ!&X4cGz=zH z4]KKNK)ѩͲXLk7{Ōԃ5Ngfc#7fn$ʹ`qe$% )ucա!JmN'#kB(7vfsIC2cYZ"[\CzWK1dP$hmEqfBFy5aQrFb-#`Wix#8ᘎ&$FplhQLT[42+6H#7D(D55#kQfXbhb Қd,LXBHE!c`pi3Bi\AA&U&Y0s`V(arܳ*ƲzU6w DF4q$xjyt^nM*(L 2#7IDM9fJ6ޘ]!A]A4(ZƐL3PCOLsvaMgB)"D3Y+#7>OMѮ)#7ayQ]:f́012R80F\D#7rg(-$m2"imR pC]: -YJUEH2 0RPCAs$P#8X(ȩ0`#7#bbX0HEGP}ß&BR1nr2QꂄjTAks[|%B!v+U,{P?|p0#4]0 F]`ATB@PXc N'零@CnT[((IUqw"$H"#8-K}ER|/ȆV p0*d u9梨ePqZ` AŽAZi *$B,hve.6&q#8s Cha|Y!,b68JhY>ND.yA;n?DA32flb]䱮o ת^ʷ;EBLS2B+jŸ n<4W>G7ݯ^pz#7~#;w2w?3=#8NF<Ș#7&ֽOXM~=#7`zr*b6bn);Ofĺ{{.{;srG~O.$a7ѫ2ai5>{48^:\̼BCX1A$%ABB'D2sBMآc ^P9#7#4j_d#4N51Pov+-9#4OI`{*# `$jwD_L??[Kb[ ^o5 (U3.c0th@0n!Հ0̤5sUHa$-$g#7N 6@vWv0H,"n"{unIsZvPL `#24 2.߿5mxŎW5ZۦmrwXܭnmn;TU&[ƹ^*󺱪 *r[C~M-%3+^:2bL؛2_sJ#8HP@r .@^,@UlJ,P1Çk5##4#4AsUO"!ԁw=$CIADA#8zsa۳K]Ů]&ӢĸTTjY `uKXEVMm $+b,H*Rd.m4ZږQj@dC'*`Qu `k(: B`A/CE!)zS{ZPuWIKPA_&eDdASnJɺ+Cۨfkݮ߱V+DI,AcJ$i545TI-)"-l[j6++LZ52AIAH_Dzʪ a ȊD񠍕ȑPhm2<EƚJ, ɔHUM$cݍ`QV&lr jʇDP#8-@.&PQe- 0#4( { $#4ZK1g_`})h@%.tԮٍí?:z#7#hAbHAlbc~p?6Vt<%LKZ(QJH}rB#8={p]kT%&0qGm44\ȗnCmPVI S \ED)כּDZ43f Kb k)#{NJw#4x.!w\Mw3y5^Z]J̡~ZEH)S*K^Y<DqVe*۳2S}n얓Ӈx}&{L\%Ioǒ/qqDq5uXܪe`̦T^]gXf'm+/(F}ȫ:7:Dҝm\Ǫ(ӈGh%a4EA~[/H~ p{ӵNV8#4p&T-xJ'LAu irkUK"NjFt㽰h;U1U,`[a8d5 9w_Fz`\' ]iC s-=S ]~suvj9zy :K4Ta1-L-vzaF)n$6<<d!2:lMݞVAH0w<381K$)UxĦM%Brdzx([ qfR=(ڵZ -@^=:rN&o֢4]EAܮlÞ.Q8"H6h1ܧ2:l]Zvq!cmQ fT-ȑZUW,4;wgxb#7XGzugI nL%;1.NO5м>9zf+K~ggsmm9!y_-\l2a$3A8ZJp?Ƿ`ߝ/z%e%L`rմ-rɛ͓B3>;G@qP3. ׎b46{noCbv3;T؆ Ehq+.X! L^\q?G߾1zYmS #7m'fxF9ծ7;BreN^55G4kOzJf1CƃA!o]DgU9sW|\OZ^:,=Jj?,2}`]-bt1q#7cǟEij-,O6YQe:޼7i#8 jLgnQa26N(!КviecWuAx`/lY0N7O9^hgdS-HJx%'zI #Q><'Rf]\n_";K'{ױ1y;4uj670|"LJ}04%Bdkt:k#7frU#o!!q3WXip&$2@#4L)Fv@![l $6|:NjXAǓ$N$:ؚ X;:iG^8pPXBb^#⨩E#4(@@DưTI&1#A0ZV_ۖZc; z1tj91բWa#4P@!c.4cv+"@¶3p&˘sN18ƪ-6A ZBPPRtksM#43wF:XTrVBr.I j,/ɸJ񷂮G&YDEIDD* #40i֜"nGSo񊱌c`#8D _<(4ݵ0֌t:6b"#80eq2!N4bf#sJ]wN L N)PDi6OR;]x3JX&Di3tL`ۮl͌m黻wwkY5 \T{g@hxF9zԮ%hBRRKc-G$ړrb p#81{zjSLxan>simkT7iLjbk)*mՏ'8[-uChL#pX4̠Ub%बuE 9w9uѦ²xbj#8jK(Jc1ycv1q9W1^27[VLKl#72Q1:vf4]yVR$ln5JF'#!?2#8T!3!^XKoHi: 5[FeUARXd74M&qqhŷ%WoNvO2CdT]^ `sUEajnkm£OGLfDeel'ߥaB#i:ўR4dIxTo]#8M֘ΓNF4R+(eJV4&n4Q#85ڎ;#hhZ9?b"fW9ك!TjJ(i_R⹣Әe=#7bu&zӚs\>E0e={E$xq8Ӱ}w躜@3!FjW(lJ=0)#4rOV#Nla,!ShGs,/-QP%>N>HgA H2(H#8S`=QLK~zC\aPJS*ʯY+4<:Enkfh>s QsZ8CuCc^=f#4_#4OOG>ķʲ_T—E]%ǖvR@"'rR*wEq'kQjdMG=v3gF?LNfF)!F#8ۥQ%#7*h#4/#ap+Ѡvt^,L1YA#R n6P ;V^2ě+h+ L0p8gj3fP1pgN&f;؁ۧ=#7~(IHEd !xfܪqQ#7ʕ"$$.-# Auh`6:C#8#8bmj1AdITo!nN-QЖ1 )R* t#M*~ Zi!rSvjAIf#7Ƀ gx2(E6n#4b3֚YHBW"-*AH#8E2#w 54Ea9"ut8BC-~'K}D_[UKړ@:LَH!6WD7fHm o A &V,EÈ4I)0,LhdaZE\2/`*Jelg ԬǍwVilcޮޏ>++2cxW8h#8uPM rlsgTqXA0?EI/;XxD_PfӽsDky:嚜-7_ml3[Ohf;0j30ӸSpӾR"bQҒ&2f\|e9Иᑱg[nqɩ>yqm#4U;8FSQ.kF]Hg0-4qRY ڞeެPaʹtԑlþ,SpC#8-;Emno"ZɖHE:*13@*9,Ѯټ ^)MQxVeJXat8G;8k\@r2n&='NJ'RˍnjaVj&dKZ{w&^;Kfv~E0Vlh9+vv&?g{w#8뜣_Nr.H"b(Qݘdކ:O$Ɲ#4ҥvv [eÍ8܂%qk3(Y/0h^G#86H9$sM(/v(iv0Pa@1g#7oρp$$1vήV<1-\ ##4;" 2Ɲ~K ;*p~5.!%4d7a΢:fOP\҂/Eѝ#7#71D.G<<xu%ʑ!ܫ^y.1񣆹fd|iZ|נ)SFsIdճml2:2YÔT!>:m5PFk ݓu^l߷082BRP8Q-h4(LI[_$l̷gnh,U@QQ@c^<9 =K@Ӵ@!UalDPB+~=f#4a_'{Y}ϙj-/MtKKsN >#4s';ql6̎\ڏdqHNDxj͡#K  %V炙D#7j䪧j"x'g.+H$9͞a#8h(Gfv0=vD~m.B&/W2>:4fdUu>,\wªv䧝[!}2_ErK!1J69 W#8\R5Uku"9sm'+;Y8Hbn ve*>2@=GEKÆ.%8RW8@2L4*HhZ(cqRPNG"hQ.`6KLj5lms *!C]`xāޠPJfH܊`9ZPc_]A݈B A|.UN`YgT$={͞Əg'eMR\߇]-59#76 DjF0$m#4Q=O`Dq)\҉W *l#7c#.r7#dA3o:;t`uL#MVLWXeGE-FXZ.ud7 )#4[,dU:eֆEN/#4N-VS+"9o+Z,YU86X Ɍ},,x(7Hd<>3%#8+")@w*uQԈ$ALxIxNN`IJ(N #2+QPsb:RQFQDP;!R'q됔g5)PS߻(b #7(9 K#7we:vRfXB#7$FVp`GzMY͐#4 BD #4#7x?^vkuv\l5!EC#8P#7dP(^=I$^VU1!˨AIut#4Q0baUS 7rK4d%(ؗWTmon!%UZ#71R+fO%#M$5U?RE6q~(+0=IY}&=UU,U&YZlQ;"!##4HF#chEk%SeooW7JRcjj+M25[;bJ^S*c/z9"_>Ao~u|%illjlF*1#T-#8I/^yדZEDx EB$T^.I<&́(yӝ\`$L'hC6a8fh.Ȳ|M ݌XX!"'6=Ѫ3pM`{ش@Z nIϿHsR,%M`L( \? P9cV#8 hMWk` ݺ6m4mL쑤049K˿kAx#4$8(VĸTʫ>08iÇJ'͞l R3oXezsKn&;h- gVfVAmm&.M< #7y@Uer7x"e#7BgDW(=#7Ez( \#7DZUqM]J#8+GۥDlRgWf\ a7XQR412aGƊhwl #4% a'8.wUK%*E ȨHԡThMl,EcB!;8cC[JTF^4A@с5#1AQFxPcQaw]> qh+#7K#32$^"DNZ]Ppwס(l vۦMMTWm;wY^56&ܕwY2nڊڂKdYywcMwml*dJlCE*rR$0p,PĤf@ba)CVil*v]^yͶiFS-K^E7k-vNgT" "b#76h~)hX,#7^fl^ڍ3kk9( kg^&h&TE\8D;!0#3`br!g#7K]vϮ fCB\y[(\``+4C\Ty|\wbjO|ZA1Fh/Yq0~5N7pyd6E!,˙zp Yϑ ]vuFޔTMN<?7ahN6T0$ςht%2q/}]21H!44PhJ0"}zj0/3GC)[jټSh +'-L~<ڜ"Jv#8^3#8.g-Q,GS5<ʥM=d0gF?s%QPӶNدZɵpC'R6yI]B8Hۏ& hz綅@Z#70(S3:yED[:-,E -⒨@2: (_Ժ!V`7SKmO{@(1\> 9a"#7Dz7#8-Seg$f&Pz\#4#4b(32[*P[B(!>¡#8+maxO'=QPm߶#7,K硐UXҊ[4tɕY!(g߰ASR z=r#4<TEÃIsB}#8#iO$URh`d{ϤTP(c`F*v#7q62!NKnS.4 .zjo4 Bηpp$P4d#7t$̮#4ĂAj]f:c8v9Gĥf<$s;"쿇|mCMp2%ul}) BA5K{V)i>N/얘U`lo*L(CtFxdf%:jI%3սQFiui#8]#7T0LAm01QT TYn44el)B6:h=Ri#4͸;T!H]R&dqyld.I'#8L֡{bѤܔ 6vub7u*3?1?zO$.>./K Db1k|^C e٠#7H?^;of~zaAFTT#ZY[7ͩ0*ʜVV"wj D+) acϫYT0<6&>0OD3߄#\}C?n mcJ#8(8Cgo_ {5 ;\PBZ$|HB%Bwqcw 54EC)qe$ D`J/fʆ$"F#8#xC(xfQVLHx|}teۼtwgvb0#4#8CsߡJ'F#7FB*$HLT5ثM&VYMd2HТ2+5]uU_[k[p#4}StU`œ\Jj#4 }09oeZ#4~ij^DdRg>TF?Pxguh[t44/qh~0\`jqwex֒ onbۚ񶹵^VXIch66+WxwZ`n],\9!Ȣ2>p(O p咧)"QW0_@bS o33ob/W'zPxO_!tE4$ 2o9[<>QGz:bPPj+"+#8;];(R̦kuvmб6jM ! H -)HGX"ǯ<|G&26b ^WV Ry}~rHO, ,X%t2@fC '?h[#ng%A/ߣvJ0 Bo泫7tӮ;jY/{=y"xiO78`@B? ~slYٓ<BY+t*{#7@F?k8+Q zg0ja#|}6;hVV?NrbKks s0rk4bzbɭfgff=-O#8.aĭM-}-<[I$c ?9q1_?QG)„Ҭ\ +#BZh91AY&SY? #% 0M(W}ښVv*Ѷ#95}wuY>}JfXHe}ɮ5MY*yu:=JS{n]in-Wwb_/ݣ5ޣ95>@צϾw`n}nk;zx{ZsՀ#%#%@^#%`hݞCUuC=Z@#%GsWZwwҌ+lS]#%P#%J@ݎW@#%A#7PP!힉G=-izqOSg\:5iHO#= e{ɽ{5Ch2wپ]co[[Z{v}eǯt릴ս=Ghh"Utۋz+˻q^slhrFۻJAUf#%hPU#%"#%{޻yizuEz{{y(#%W-LI#7_yw \`>h;ic\z=BۥXڸmmʻg}M}rմ{;pO^#7]wn7r۬tG.ٶq^O#u3ۗT-UU>y۹{y8IRr^4iz[cONZs7`ox}{`ѠAJMw5۷`uUX ft{y-OMdWu{ݝJ2N5qݶ;n#%#%S#%{7.n=2CcO;&l]5vO++cQ8laIIJZm-vJc5Kkd;0&#7J*3YNml>]nho2P)I%$Y$HƪȲc m3f`2hlPiTrAda#7)*|ɯ%X_uBQѢܼY*w"a)hRC˖J0 qMy;<v ;(aB!ұw{٧]W7RSo b=,ZnG:]qfW6REE\ۯ]|EBT1_7uJZS+䚨nC& L6T7a#9+s+pBQbuC͖#Ły0O_|ל#ϝ^6Jl#K\ T;q!2v9$ߜ Z1U&L71']`HIP#.\ zU^ 9VYj"=EDO~|̋yT|pC#7i99ؚd=r\c}aĆ%[ <C\^:dk^Ftf5a멲cj%8+uB;9R#7P_BE`yX iV=1wfGcPN)<,9Ï,x.'#./}̗s0y6.Y<5Л X웛Gq8A{Fxl:gc3uۭ9U##Wͬjz5sf:E5CN93<-6C~X#7 `8FW_,5(Ho >ںd3oWg7B =L{zָn@AL>dグE#l1bÛZ#7[' nMlS1[pne<a;4kxvu$@Es#9IQlCQ=h7G㛾opTN @!.`xC4m"xM!o*{zDq==:@[\x`iPQzƬִ3hJ}#9ӛxql9ֵd8!`bo#d_ѮoϡT^ ]ȃ֯/N>+׮#94#7LW[#9EE`_=~uW1>{~nF%e3Pov[Grlϋ:Ga$MvLGƎ y*V)](Aj bX+*v}p)җ 9Ip}[FSm]ǿJ26Ҡ^TN5v6 ]ضo#%ðXO@be4aɁB|?4df١s7 P)#9ʿ>f3t~=|r=~<A ?Zu>t#G{-7mv[WS4n$Bfe_^|!eZjC.=G1.qQ2f^4i̲ sGh;hXR;6a>(V5(Ig^L")C m쪤<Y-UiJuJ*M({6q1R#$MrQVi#9Ÿ~7f Q6/ѣvRDb)HyuǍ@SAB%.YJ7g6T^zqdt ]:Cw{GXߌ 癃cZL\7M3t}Wg#-|ݜ"юdzԆBIi<@t5qHVv;^+u+\o2A}I3ggqXZ^53&aruyF!{ώ<"skȰiOL!r"b6@2$%]H~U<0PIJ"bbENZX'#9N>[b$ʨ0=+\R/@n}'P JҿT3{ @0ՃEh2T Zdx&qQpЏ'cDCXݺp?'-ePH_Ի ,:%|t7"إU/qˏ #7(W7"Ƹ%DwN;:h7ZzDS\cىI_O:ߌf 6͊nVqDD2(V@SaF.#%{<q:dL`IĦdͬ=͇b:YRu/5uezey],CĬ''G,H^%6eոN+Ϫc `>&7hRq yhL;': RY#7 `Fب1WҎ8]b#O YMiroR)9:sIǪ#N%]M;#7E>ي";V<(F{6u%kxBE{3pg#JfTbޖ.[ގȹ$XJj/vA!$p|As8wgXq1e iH!BAt `WUIo\Tkx)ghLl g,@!oX #7Xe[iUYWMr(SsLx8L)\)Y)Vd9mɬ+>!Yl;ݗ5`"[װ_-*_Ŕ[B@\dSV҄i4W#%AB;}Uij䜐'&ǖ㻥o9.pNG{oWS9=:CÙAj:l~nͶ=n6JhP"1bL`ȥ36 od>EqYxg6uZ)2UTC#9ZXx$ilWƵD Yz=+:ݿZf1tG0͚Os7>089v%xi-%?#9q8[bp>9<20m|=ebot4 J:tkXhq#֪X*&0^j#7DGGgYb#곆֢PlR17G=u^`+*jۻsxE.::/>ta0LDP@!7P6"8ǻ/H S;8z͇?QA ;2AeHQ=Ha5z#%lKaH|os+렁I7㿷j>i6[DKtoۑEjKs=slƛײ":Ӄl#7:7&, 7e$IjP8y6fK)cLg|mì#7eÔB$D< o5J.rKm<*\D0-K垄lK|zg4ˋOS:R($Ly|`~i^l.k%Z;m6$x=E!dW)L5le&ɍ,ŋ ȇǂmBB۳bݵ ϱA=Gm7*M&\ R:qiϓ,oY>:rRgtfO UZ*" JdH*ʛA#7&#%A3: U #7|my%^_FIS`w/ i#lc~F#7qfֹ>u;TkF,̹cZ\#a(=_rȑ;:%~I+êuyvZ4O#9SZ.pՄ[)ILc|ߕE۝9i1ǂܚ*1z;2.{9 ~Mz1}FGIuVNyt-(iSIQ,KC#%UKu%Z)xMP:BLn@Y"8Na@R4\|,][ W#+;}hGC#7F*8ju6R ~ϳ/ɗypK#$䆭o8ᐾ3ݭ;htlǑcW>K#%A[#%AgtHt?#%#9imxL|JL%uUid$m_~~naMH ۤjJ^!W}-E/:&n`RB&p?}H n؉] Q#%m?E|AF3tjP7:!-ȺQk#%_ؿe-[3x! w#9"rO8d#%ΟukyɹIN#94O <ۣP]eѶPӺ{k1U+{fȖhb("&0G waZamA}L"ŔfPt1H Wif#mayo/Ϧ|u?5?O@5dɶ}g(wD>: ֡ڳT[0a W#&""""E]HTFG wѫ`xcuo9mDޘiv(Kh߆?GDe=ɑp]H1oɆھxfa|P[F)Owp`HE D#%ȋak䎮8m@~'nh&M6J"#9=;o#77⮆C5g՟ѝNk:y,2pRǮ2S~8 R%]_:n1`YA#%'hS'!?.0?ַ':<%ʪW#7B`ӑd9=9G-u/)㦬}hMok:X:xq3Wmr>K^K|L@ˏ:KxqJɸ8LM&'ǗC<@bKo^3#ț70=˱rТݩɜV]Zh٤vLR͉#7jF1p'[ɱs mIbM{g靸3u_}N>΅p+!G(hVK+]>2ӆk,e̫?NLMEN]ۜS|͆. _od_Wׇ3$57؛FhHKF+Z{Ϯ,:xM*]~U~; H疔O#6Zuퟞר3|ФFQj䮛-B! q0:i4ݷ/w%Na* ?R3|GVnWƢ+ !1qe>pjnB]:Õ~tˉ7d"Қ#7hi݌ $vzG,XñN7B2%<>,v)pDI:T@HM#9&rK]eB**(EJ .W0f4wAq{q_ZÕ)9CHPF22I&繵e w}`vĪ)t$$HJ$?ۛ."LGΗuq$?(PGhPB@H@(@x RN<X!fФv4|0a{ˆaFWHqFڋ[Fn#7$з+Lq.ԳPxZX4J5 أfetsVc&=*5A]amH8Y2ܡ+G”nޣ/ڸ꼗`Y!ZX̐!*dxO#d'u^tT/%WEs6k)J8ZQAzz{r`pq#FPӳ]صNXqst mf.rQ\Z C ShCn,|{a,tKn$:;t+ss8ě$2>Kê^\6W4TkseIF'^_<.1/oC\C)4EX^йz-=9&fx W.Ge&ArvK%}g䘃Ԍ"FJ^^6tB%]ykaFn&&rT~i}7_9^Wr< aHEc*YUJbSƛ"#7Ikҡh67.JG9^:_+XgMV4! ә˿|3lZ I=A"$]xil4a%7|+׮1ӌotF3Bh#AB rq#7; &KZҐi˱h_>i 8PS"o*0#9x!1"u{:k4I#9S2X0#}~};nbe2g[Hu;UMSv{T-1D'SKȞ0:_\,#1A"'Kb`.#7"|%2E(Qc" "##X!=YH20Y3<6a)˪]5xūWbƨ#5ݮQ%^hAd|0Ǎ+cNꥍ '5x}1mu#Rk$B@ 07v|Ճ$n~>]8/߭јb#9D!V#7+γ(袘Ճ7+9˔06~![@*/6#7R`̟V˳Rm@9_"t<`rtǀ"z7sO}(Sb`6{m}ktp_0{6Ws#9xL.N!5&faz%[CE,"lu,%G$wަq^_AT;3ڨ*8ƁpϏ0ŷA-RMk1oՁ0BG17xKO9øϾ4<~1zftُ{A(7Эl$_lD zz #9!}ƥ%`jۿ#%Ưge$$ʝȴFe%`!;?=]E[mT X%K`O p8bv0{F%;І,Ǫs 9O`p~@(iX1 Z{ ƨy>u~EFsj)IXuS[$pllrm^TN Ҙ0c"KՐ+Q1B& zRX#91'GZ,! 86h)cP##7c0\5#7(F41ݾqX3s{w]z^44&iVAcXphFU4H004IvQ##7>O۹BUWxwABcE#S]=LWnV:OG.r>`%;Gi%5'jn9}(Om6(ys=莙>s~NHX#K^=1#7,[ެrtd=Rta(Pf@(ppLݑVIXf$<6mC`Osb-l n@PK`xU8xqcGYKg=q:dVmYY_5"Al*\r71)J:#% ʑ`$D]ɕHQC-AJ.vb9EP#qHI!G®09ǿ#%iDPي^^^(8l}qveAێN#7JAax`7 BVgoɴٛE(-ZQMh#7 R!#7b9ezmQaRQ/pl9kIߑv2fgsggbm\_ꎰsNQ?{zD#%Aw>aks!/\x?~N^(0du U4#7c?~el";OبtowZ~axj=PB?=ܮR'涾pq>|HRCd=|:'|ݏNrp}ry񳓹grp_ϻ&wWq&oy#;>UDٷ~t;d;FU߯a.Zgh#7b1:p`wIS`w8yJK.>#% " . G eR,1J0'#9bbǬ/2"XГ]@Oqx PKVQ7}DٳZDnߍx=$#>_e|Og4Ԋ,-3uh"%D͕,J/q4ثcb8J#9B1%/J#7%~!<ٳ#%~`DhPg#ޝ^rhϯW!+w]m!L)bҮ^>[g?)X(ChJwN5}zBez),灉ȰXOHL<o~&WJ@6)Q]g9n%,lCaxuM>/.O:CH, %Bƨ:'TJ*0Q\oydQpPJ'#}~$(LC?ͬ5v;)}i#7N;~#%~~)#9#7@C2@0R_ԃލ>mo~+l}_}Bϗ|ɕ>Chr9Tt#7r߰m TMMV -ٌGFϩkۊut}~sǯOt _-/2~2Ls=:A>oo0pb( 36< ]/O_ŒEdj4n&v':; N.ycgcjJ<3CɔqV!2GĴ@߲?= r;/b9]}sdYqUByի0wɋA#HlKG1MHҡy_ݕdGLu:Pv 〘Hxq ^u 4,'4 F#7WyUE#%Zm=m(u'aaW`,8C.!įr^1 PuqC5'iC">@7I@0A i}Iˁ}r֌?X7#9pI"̧9a#9B(@!pK`@ #93ƙMo8C,J/7in\] =V]F|#]i,-~l+vӆ;wk懯Z懇]]av܃_'.~Zpߔpdfݱw72`ST8*dۙi(OfNyi*c"(/͟tO>o#9sێGOW#ԎB$h?EIz:|[Yas_゛$%(sy.P;[|<׃7>wɺzZr94l޷Y'>ax#@Vt UqeNL-t6Ǒ}CD'-Š8~)_ˊ|?.?'bya냷o;?#7~1{xqӷ?0j;=ԤTq9EW@ͫP}и(?Vq_g|5owl{K3_}~P }Sޯe8]"=݀9#MV[;?-m>aϷ-[b>~M{2|Jw#7~YyKK﾿@di,N{ ]ەtӦ_(MP`ƎG3؃%5(>#%RVC3鿻gCJۇ5Ӑ=}u{{t ak3~݃S߫re6v]:]ڑ9: (4uvX'ol{?vg{PϏ7~m#7n kv#%P}#Dg>^:߻|j6#91^`rO~ Уɕ㓀9#%bU;GwrD--?3`S#%!*A{xl.K#7ܭIb|'m*O)HHPӉPVj4t)Fx$!A33c HXpOn Y*#95J2#?do;ۢϫ/ӢGzD[ytmcw!*U^˲(vK@[Džeg?GI zߧӫgyDA_o@>㲞q\sgw'Z}S{=XMhapa6)-ow˳_#7Wp&vV}{- B<5a0ȧX|MnB+i8e#b`p~z0X!]iNk3I"7]ޫԬ+pd#9l)BfSPm$U-G#C:>5z^׍uG+LZ xs'`R1T v] 0hي sofGh5S@:HƀHpv|Ac4[\I!hrHp(֔0#7+%`aԤ56\޲\J%8 kqdA QXb+Ơ F5J0ԦFFZF1W:?;XbH)$JC?D8E/{z zz^" cg'+\+wiU#9+'(W:?r;uթ{s(hۛFU ]Ўxz(=:4®NO ds(HNzh!<=3:ǰ/]/Ѣ>ǣw]v޺m_]GSpjiY?Zu`ܢN⣉u}<{:J7>9fԣ7 ؽ~urIfUUkN ]ZXKRVڏkIq9I!Pu??zx[g(]l9:{ 퍍t"<=/J%;#&v_ɪ^}U+dЦ6XP^cS]y$䘟O.^ַ] C#7#pwuSQ Tf@nwO]\Kv CP<16/.2#9 eE,C#_o/<Ŷ[;6'ɠV ,``gbFfDQs(^#9A3#pjL0m0z 5[1D'yw=tU2#9#7FƱfhPȋQ`{9ozDbk[7y#9oEa,Y 6&Kv">ũ#7HkrpAT r2.e8G8|h(ڬի!udL}2&`ҡJƛF ÖT6hL&약XkYX7ohl#%2fiB?ѐ`uab)J,2nV#9$5O76BF)AI0 P;yϴy@NݔHT#9-ۏN]!MdYd&]/2pf=Ae]?XC.>.)mEsDLzPOGsѻ_6_P=@bG#9#%x{CdH#lQU?]*N0_u-Ȯ" Ë[u1'O|4UdNG#"DL>91XA9}sC 1ѢOMg{l=:(zjLxEz5#7%Z %A8#71ʝ^sRIBuޖwzCa:F! cOPwy}8H-]$1/uсB!Cʡ9hqL>4.ZVFxF7rc¹s,d)"d0j 1L]i@mPۇ(PU-)cVR7<9l&=U##9icqBoFMq!ZfwD7aqcYJa!r ֘q+#%5DJ)(U@CHP[!SF`Pv2Caf!cm/ KoRMZ1AdF¹XC]$ 6DDyh4iNZi|QjF:LȆaJRe1e2W*/]^.~o>! 4 -_ x~=w(گZMbϩӞ~_d8w=2"ʆ^е⁃:rPvjNQgksZwq:Q#₪ucMI#ԋ`⹘ڮoMGJLkRYeǫUL#j$MBwc ]6(#7AQo&2^^("0FT3g-.JHl>5b+[Xë1{qۏPnh@2h{Af?)D$#7vqi`:mvQ<3{L! 1IΝ2`E;XnhɈg[,sP̙$J'xiW9ۀ#7[PZ~bJ#7~5cBdBm5H[rn37m^6hl GR[pFxTmX`[d^}̬Nm0d@ar2tMp[žqlvֺW*#thnTmpZ:uQvtwPVM%K̅sIҸeq#9OX#7RrghDbG̜ʝSM| P;̓7Ee4#9lN#8A(%].aZu a;mde69,c$l;xvbh[tͻKϸ'A Cwnq{ₐd:)C9+O/_|Mi9ʜ=D^"2Uǖ)St?ÌҪ} 8Dɻqߵ41h*&3Jw?νTFj &WDLbij3S9#7*Kce-%œR\m=tӘBѣ2gJE/X\\#9rsu\mV,w&M|\6 ]c׶uu'ǗNcn|#3DLb9˼1Z꯮iT' '^^JgnG&ݷwJʽwbzQ)G^i)<X߂vX`*@jyMvq6׋^6YtN1; Քn3{jTMh#7F6r#:LX鑇"4&{M鑨Eּ|xHrբQ)DJifz3\ޔkYn1AU5ٟL2q༹9w*39w껷'@Up+F#7]kh1TGI:P#7K~4mTUy*;}n߯d?Qj<0]}gݺ/G)+k<@6%M.>mڍgAdh=H ";EΗx7b+w6w;5e A"#XbJ 3y!ܭ0#nrzʃ{Wk/]yIp.s>W_ vc3&$!?[1x:rjp{jLflbiH*?M5zO2gq lgQWG'p."Xp ۜ&6Q9ߗ#7?R0A\dI-:bp,~WĎm&I1g͕luazm> L:- @~p^Dqן i{߯MEBkxiW!y=f|2q "N!C&vm١gQ.+mdžMfW)ĪB{/-|?)(#"^`ôF0o 4E9 wс3Q#%hRPcz%xVwk_)9vc3a;q2<^ƶT+X8o??3Gfi>RtK`,:*QcجzKnUWA`a#9Qtfd49(8INDaDm|mgמc@գa$1NȌVj vM?us?f5uÔx.Ui![te*!ĘtjSsw{s_sz(OJgtVQsAq &=Ze-<&IkUR-r#k[H *M`Jҋ4U͡oS_N2njޡfn \Yɪ/lykA(ɈG׃Bt9Y9h>TdPX6^^0bH>y;IuEm1Gkna>Zlz?43EHvGP.u5#TK#7O{TY>7{;r6sgH!x& |"q!i:d,YC[zD= *\QΖbo/PImЊry/YT N)6FDcɿV܉q!'pg#90L3DݢxW!j޷?t-1ZݝΉ͞+~#7FUzFE('WKۏ-$?: ^v0)mZ{x]#e-&a0wB2/3bId4Z?φ)*{<#Lh}B%x<'#9b̌6aГzP/46z9Aҹ7Rp6Q܇ÓP~1n}!~?tGF",gZ4(J`^#A NQLvwq ־['xSA@Gx[Վ, 63KF!coĈIe-V 4tkxվHq*Ρ`*3H#%Ŷ~G-a]5Kh7C7;>,29qxz֗\N;o֤!B;<_glD?nڹN"^Ut/ //q#U\mP.Mt]Q~1R:0Tx*ci} 7{x\q`IbK1l@/!'7oza#%p[0Ѫl$*<}9;mNb.c?w|֢٫ BsYn3h;Z RL(wVnQ.;Z%%0 m#9Jڨ]vBb!f.а~Y#%VK&gV6@,۰ri<2UrP?D'tRr쫪u,KިNMߜH"XM久LB벶#9:xQY!-/O;]$Q/ڿ8z:nF83Jsx]|wߛPbt3c#%NūS`4L`qq!q;ODGB="+Mm8EbW8/=Z4Z3np#Ը+ 6$[} ժ2?L߯Zr 2SmtNb#7#%&gs[RZAr([l/\$Eή"u0~M#%#%yWBfi,a7s(T-%#7΅m4BTTb#7kފY_+%)+^OÊ፸~{s6<*W33[*SZy4\]=/a PPNIiD#9Ut#7ayX7E y?|sd!ums{JcU2:j~^u(}_3j8P.ბIuƒ{$w$tc7@hY#9p|jTA`BY(Óla0k*d`M21iImQZӾQxaJVA,I\9QAG:)3J$Q^n58 >K%!k'0a+VS6vlwUN\1#7 z^#W[_kl+]ʛYd8AH%c]ב}tE4-C?DzSrVL1Y H0n<+~xbn1./*Bsg$۰* 5 LjǢ!+˶_IsE%\\A]X;<:lů̩nR#9ocX}Um#%: :nrւuc(;nS:SX:{kG}nrsd:kG #]VQ@FUF?LUt6TDpϒy>.|Uܥ䚌ƠAVA*ej'[x HIecvEG*M#9$9ySXT_B(PXwe"e#9`^ɐa3՚Q`ȐD 8N#9A8AOD@:lәky6uU:52ɶeW9nό>hzYzr=k?(inPtъ󯔼N%S}}*qN4Aj^{a@4TQ;Y޺(5(ߧɈlFk](ʱK`ֹ\Z _&,EE;{w=:o'H-gHڝ#%tmg%e=ڤԵ%l\VCɤXIbk֥u'55t[7,c»=`|r_~yv)S߆qPbn!u@CC=KJ;HCFNpXE+#d9lrxE(ko.lHS鯐F(b6v2$'3/ 0!#%:;fvISj6r:93XgL}vsVKsP_{<5#%KvYT_ #pkg وgc($*qY@zC$trGʓMs~b>WًTAKƻtT6 /07\.3BP>uJm^kSҳNPt-3B*;^#9gLұe00ɷ:N C薥zt%dsL]= D~(#}H^]gǛSu;(3{ޞL:{ؽn8G!h^tFKm\/_v2:9,)!Ґ6ԓܞl=2b}dWS,: 7= cń` 4n9yC\`;,>4#9a#9u1/W="OW#=,'7j|tK.0Uk1::e~KB[i')K@_BUNXhYPdd+j6YFBWrgus6dVYP壤֓*WO(D>}YkaɆsݏAs6,W$66Dqv0}]o̼"#[B^c.Xr +zټ#5YWgCY U yCMx(ltԐ|WJh@ZC0Ky [X0NYpخ5aŬK CJ K|G C{# G#90Pd24Vtz3f,]ĂQٔ:HLnkLaFJ(* IT$@ͷbW#97OY:sw;gKtb5\ϋBM]TI5Y3ݔ,M-H'>.> `fNAWV#`㈹in%]d- l(x=%s%;'ͼh>}|znZ1[U"ٖ]5Í;(/#1!#7OeҘ{(躚 y\)؅$zM_rEi)X(9Ϟo| _r޾>3#Mmۇd<3{cӝ0JllIi;'"}z`'뉼EGv{WraD 3zO98#9cd IN*qtґ#7J_iv^]+U,oNgWI{GG:kGV|ɜf<S8dj-y=& ڢFcnS[B.+?s]yh3儲4jf3}sٽS#9yا+i NXۃDѹG`:0^@6 %|[h1g>?^ &I(2اmLڝbݽS\wN]f Ƨy%ͽ`\7G]&f]ǫzs#P=P=1߬\]yQC Ht#%EBW<0nW'p#9a@|K$u(J! F#%aC8Z;W #7M +#l2#%`SzUOH4Yێɇ_ѫh:Gt9۠^pek]#9B7ܽ>@u([x#9nGT@|8w)۹Y튻H;ȜIkB\,US[X0X4;guo<ڋNY(9úV!b)6W1$#bMͪ΁tTمq`%Lʶ,"+=<FK+3tL+)э|6ӢksFE]}MUǿ^a#%'j[w|@xz9},] zlZOؠ#7?.<:Q]#7(=f´sp @=k%FzB]尺 pPSERH$~+i-cm`y19-#9 ~ZPe2bH @E _7(; 3(VN"Mꒋ)шBav=)#%"L=g #%{#%}U Š #HZHeh@c^j6xīYTȪZH1nWC9ըU,U4az-Os*H=D-9ﰚ3q0=,w2O5rXkѽuKh?d[jہ1R2jkЕrisz\D|#NjvvaR^cqNLi08ܟ\u1u6Z.Ŕߑ2tTq»,$kb@ *6-Qj#7;/ۯตhAKQK(Kvz#r hmzfM3g #%C'4Mh7/#7JF "*y2`h-4@C=lbRhSZë-8u l0J(#% MAӘY8C#%CY>z;^.]OAv}_{#%ٌ,FQ~I^L>16#%ugVg֡|_f\ Ȗː*86Sϒmx6wQx520YU#7훳@F&BTvkKt˽%6>jF /%' =B9$ک7xZ6;{K[(?ޱx6|=>z%U(Q_RFd[DFxcq#72;#|9[]3f+k*zJYZ#9~{f4E<}ާCFLKWc.{UM#'q2=yLƵHy!&IB,- bi#%#%0.%cX,>:ה:l{. Bŀ{ `,z;Q\^XMgӑχ۞XJYA,,D㻝C !ǖ\on]ywmB#la<UϧCp8(9t E)癖vLsvP/CNN#71 HC=#ڤ=zzݴQ^/JPc>ӿ-ͺNM?RZ0ﮂ[ۜˮ Jܗ]Y#7AJɷ%EBZ@p` G\`D;'Enӆ0(J 9E#DyrI„ Ai#7(nxy]Gy;4SD5wZp'Y#%T2l`̧9!nz@։/,8 8 q2[[2;fx DP#7!F&!E1 K $H@nnK$#&|`9`_#787sqS B]aWh$9A2-$䙨 BTZ/8й3cux|*E8ie})ݺBVe^W[?N5ӧv [' tp#%PJνqdDe<2AXVeܥV5*0R Y.B+cE vCCN9^dyj]%*$~ia8o7:`_Ω>hM(+%B:Ӫ[ϫBE.mI(P3=H#7y#@Kw[ܐG x46`>`<r#7za+Vu죳5ly[hμ!v1h)$Mi| lˢ|?.v5#7"$&~1Oq}Lk*woe!f nۋ̪& ,+v 'puݧ5||ۦyO2}Ý1~"Y'$@Ni(C>,V,#?cO}SrڟvvҠէ#7o:Y 7[7N4܀\|Z q#%7BoF3bnQakH2+2x>WrÅ,4ilJ~s" iZ铵#%!P0XFTX#7r#95\ف {0aDZ6gvGxsb#7Na bU*%Q:l|v9rk[9pPnFp3=pxף5Ik<W)J0x%x3c[ܴ[盯Uxg_ޕŞ'MCTնzb/lZa[vRv,U`@i,#7?UCUTH>q_CTu*zm#%>#%|{tz^`Rړ֩ϯ,TUR`qxD\JH:o. 3S#%9-#%EcTQ-A*(bp}=-'C`[2τKuc,B>\/W)5s'+s**#9M5Gwv9(sdV yr燏H,q=0܀zX?|D?7_'~???_g?]-?s~5w_#%}@? `@l}#9$?5Чa QPrG#9(]!f"DCXիMD+hdjcuv!` E#9`JRPVɲQJn),dEI#%:ܥv#%Ѕ+m%ĕwq&"ji^b;F% Ȧ6مBGՔT%_BO?Wƿp E#9Ń#%=0s8woCFÁ[. mcN4($ sS ַL%K#%uzg7]-s!T/{Ff~`|s{i ]]Sv-mjwhaۖ}{x3f$1p٦n34Xm(·0cw؉LC/U}*|J`E*IK$dGt4sݼ02yX쬹Xy=]ҘHCGkM =9^np?ajTv? K~ۯbi,%3J[LS>euUQdhOv:6"5Vm1_Kvjj劷6[V|f B#9H RCSC S$]]wx-Қ#7&F.M5f Fm31Ã6xؒvIB>g#9j:E2 irx!ﴇ)ȥw fC&HMDhe RskB{^ñĵ>Xd>>WSDT 2 #" nL/"#9r\d ǎ{ޒ"M=P\Fq&gh\Ӻė*ɇw("$.flg~]qb`Aj"Ed#%$%gvoB! J R!;o<6t_}yVd !*UgiEVj*2٩f\FMe@κuSw_c{9ňPn D8spӿux.-Zw%fQG2#7Z6 #%>ÞW@5lĒ|(N:FSbHrk|aʡ*DZIL[@4k&U,@N`#-[eu劰X#7$o`J) :,#%wK^[EMU{QhPUz21*![iTGWwJEVꗾ^z*p#9G3Rf#7`4o[.ר }s$iCmҐ#9#8|٢IIfH%[(0 Ȼ=ww jnv"gwӊ{O `өVNoNL ;Ηm0T&S+zh6{Q7vO o¦cAjH%9l̕ą+2XL{#7~=/ ] iU aׯ'wZ)Gq[yd=f4~C!qSi֡.hfJ9#9aF法3is1f"HC0)z(p{]*ksBAT곇/w,o #?e^]f>%x\{99@0n۱y#E -#{\)TCmBz4=QF#7ZpI(VcGeHpp/#95rc|ֻBzr3EД7ADQrc׈È|;L3Gϱ"@,=Cs ErȚC=d.Wv4|oIwKׂmkgJ4CUhtU\,J!|;k{VqDbɯS1y0Oot#7m䀝vdHwu ;>^i\<|cz^nϡB`Fb9ٰyl-0Z /x;cl@Ait '^#7hGQև]CzsD\38BSRl=C|+a8=@0aYmtsB#9:zO$sP+B Tn]#%'zfTOkOBdd{,p)qEЀ:l~4?7]#`m3#LgT#7Uy{L17v=G/W;٤5n}a54ݑ}#7\f~k%e$z #9AQRA`h,]g-N⽬(@ͯh92~l{=%s@ExBɅ3v w"V?p<4U4$/aQS+1N~?3o`~O.MHpZ@IJ>FLִPػVݨ "" +"*.]Ͼꖅzl`_͆f#%ebfocjkZ@+Mُ?J ͆:#% {~_xt'2!^b?:k>99#%z[?8RAhj!ޞ#ſ/C6ϰ.&2YÎg$™ Qeܡi(}?~03 dMbaw Y?*i[;9-gulie{8ߴ|}3Ѩ"$` ckP#9}s=C'CJ%Zǥ0*1EG˒D~L9C'͡ҀXDȔ ?ǧzCK>'tӲٳhHPD'&ӡΌ9&8;bΚ ȹCLJ?+sU-uuU 'N"QpuB߬m;%$#!^c'^]y#9lN٨q?>#%?yuexz#̿z!_*|Dir_" %wN{ #9qFVnT-O;XC33AWr# .XNKأR[u_TK'#9/ޛS?HP7 !'MHT#2p=5:`OFd{jz,|1_#%?x_p|)難8Z#%!"/yG_PПKÿ槲~\ײ~frQV=^߼AL}S oGB#%(QN!#%aВSh#xD-O#"n#Ĩ{l31ŧ*8#9 #%Go!@=Oܞ&ܱF90I$)Dd!9#9|_9&ȿ8Ps5f?Gz?n0<Mp~@q60-C`~G#B#%IFsCw#7̧K,GOK9<4 ڟ4`@EN>dJ T]OG5V:qK@3]W%/逵cMD#%x+Eo;l3106y- oWuznsRm>;#9dDyD}q5ӥRՀ-#9|vf_ 1{IA޿/g#%Mv {or۷0-=ߦw u9˿]~N#%u.#9=}^/⧍ й[UQت[;ЩU|p|*|-,|]]fbR L[z~l36GE9WyX)Ue6Vhc}#%~ϟ`qxb܀tK#7Ou,z=Sb8_6m~Vf.>͚t:9U#%(j&A[588NDn/WhP#%@q!2(#%u_kzw@2UC#9Pa#%Uߢ\=ӫ䄬Xh(Ex`<Ҝ#a$\zJVl>{3$u@0#cٓ;SjWw׊;bG#㼶J;%q7}r8[$0DEJ8/|? i)~zFNR[zS.Mv]R~ztW{ֳs._gici1Ez/ju qWT.|ܲ!&S"*ޡJTIis4G\V b{O"ȫs@@Q= cbT0rHAғK,7$̉Қh}41z9{!,ZT:#9AeN:/z!v7WJ|M^pV쐚4~AO0m3LWQz3uswnU6#9;2ĆX5fV@\_qgo2åLVyxbDs$`0ݲнTk)pe7d+rѤaiWLn+p6MW,%M/ImvS:+yg[1[|{$CGHl/EJPΈW~p6qAN?y)_JoNûј"ɞEg'>E^TWz+Q(\xudя g3֫JxS]߆6 !B]w6laI$`p[7i*@WrFk@r+e#%4VUs=7{حw]UA#91^M>Q\M7Β?W~XH2r$|ڊm.];N4,k,Jt#mbGu?Ax8{/9 D9ќ e'@4O(vvQtJC9ɢ=N[Lfw_4mʩGC+vώ ;uS869M؃؇uCo|㄄5c:Ӭ8P}O/}HiUe;BV~fٌVy‹V͊{{&6Q;}S[?B%%8mEv-ÿ1#9Kܠv<?~ڜa2ʤC]"chh*jNň#ʹ^W8=`EsssabŜzbg"4.IaYۋH6K yvtKe殿zv"<oTS9Uku\')uGP5u2sk֬ >YHPMgP+~a}l!Vab0eHfqd.Ug  E8#E"SirNHhQzS+W>mtEA_G&>Vn?M07 o״Mk2DaFzW9el%ߋ4|2ýIF&e*#76Y UBH #9Gfe40p&~#%[hݞ$^e`x B:i#9DUu]Ombl6I7Ԟ /}ŶeIt_pg˦?n7wqlZYDLGM,P䓤1Mp~'bSnD]F79H*#7ؾnӰgg68PV\gi\c7' Ofj10E|:^9ahw8#7vewOfB&)ɤU|n.4`5j[L>r|e~O{x Y 6$>e% :|,e| 4Lc:(^#%#724O|uLiFRObebH[DOV.Bmﳂ2WGHΥ LB&jO?2-/ٚЖ%Iq@H2D$iO@]$Ci]Lh#%}#7c "|B4Ď#-"b^CNȁ#%D^:l9=kX8y!ko|>L,ȁhY0V+.vƕ]ALGG9UЦIQ2ByvbSk:ւp#%)GFzp̕dgtu<yqjY(r ⋗?߃wCS#U]=t mBC韟V4:dR,?gQdD,H~~r]V~~N)ΫJF0?%s_8:I{裎+CyK~_ӕG3'P:~'?7.x#7~EYF~g/O=_h@{wx`"G'u>\o½e/s ڵ$Q99D]ҝcS#7x%_ğpƒEt݌9^pD}l׻u0V)ϻCD#7c}+aBJ(}^;^5}{薍Lt0HXB"TH{]9q9z#%M2/a#7! wΞ sV5Ė#%8] JZ,\Sdpԡ]ȡR3|T)ALCVxDDݹ# G3~:B-PP߅HymߡOW^Ğ\iD1̯mX-D:r5߿=N|jEL}[o8) 84tL`bF01 A`9F_Va.#9rg%<ۏGp~h+2,FuNYF`ehy"RHc H,*B]ݶ>)Λ̶[6Rt @g4( PX{._fD#7H.},謹=𾁛6<КйA@H] LQё6K8qҴ-s%߁N'őgu2@$$F+3"7Pi #7X]x3ugYCpBmo2`" -ongEZADZ#7)MG3yql_G݈d'|ЦSH6P%54@A@#9R8.us;ܣ##7Bz3+uWR$q%P^V!h7>yVU̬D9˱wҨmMTe5Cy`ixҫV1c\[rdNi?(l?(c\ d6;`M˒zGu )ч#9t"}6yc8"Gx#7u<$Pi$d"϶HBV&_]'yw_4wI^xYO-z6) J'"9]n(Eƙjzp lE|,|ޭ=Yuo#%#)T祑/˓NψQҍ;;fµ< >X$y.a^y%X_X0^Ho+5) Y;l P6BzG0@QHM#7saؐ H$y\, Z;bN|[nΨ!Q7oH#%q7_r>,RT#wYo]#7 }Ąl0&Hl[oyr@vD!8Y7V [.,$8p#7#7/kQAԫ{nGӸ#9ɽXGSc'Wy(^Me >0|7ғNP.=$M5~^N扟һcjN*#FnF&ov;N8ڸHYoVj28eh"9Rxw- B5EFIJ(h3eꖇ#7vHD#%5GqRxOIXsEf&Q/\ɱᓵx;0{[Naê9΢LLf7j;A=ã֫Bz±һVW "PСFNWLȦB»F+X|^bxrD .>~-:>?N%UV-#%?r{ʹ#%qT"@,O__M!_PI(, \#%' PDwޅ7a%󥨳K~9$=X+!jи#7_=]FTZ3 卼#hX{a93JS qCus HI 4U7JOs(;I,w(O˯:jٴIrOo/܇Gv vy+lTbxލ8zY<gAB1wDRAt.Ҕ`BĶl"RZ:6S]V9'T-^p' {W8 o/ sR4J͇.}:uAJ`uw}0ʵ@ `";*/TPՃj#7"TydyЯ "옓Ô#%) ?YIJ=ro~)!WVr/y(2dn1\2m%ύPޑ pȢ:m#7-߱b@#%y&晵>)w̉7"=[O GzfjgRQ[K$'i>7WvO#9+jVy%uB#LhBuzyLYN#7,=r"ak\o#%D آRˎldZ:KE];I w%h/N r>g|k)z_-B5D_9y/fݬý,'XTSlpm̲w~GNtKh}\}2)bO2]R#92<jC٭@,3D:ssIU\&ĵ9˕[%8/Z'*.g~oʏz\[Be$#6R$<n`p FC#7ڱJ"")4 I)b GbsC(IjqP ]#9H9 -΅I&JlKK>E%g8uQVuo.?6IN)a$' (Tb5B,ӉM##%-[Lz.nŁGm=}NJ_,~@XSQ#9D[7~o䝾 .A<X"Gc42Lۺ3.LBA$ℸ2:, IEU93i(@R/X`T)ե^j#7.wn"E69vnd_sdcpJCEM*rCpI#&4cer@huגHTiRb]jiZiMgbsJ#9S7.N|M`ed=!,p}#9fquo0w FI*ai(ߚ3yMҀwv4&)E~w#%x;G_S"{;I]a[9DŃ y60ieh?!GLJ2f}19[>d*%)H)M4!hMn lC| ݚ #%@Җt~vwy#%݁ q\RS@``\aXd-)8X||my#7d<}!l ܬ[? Xy?eDG0ύ_쒊 pE}VqdE5,#%E v=4t0}3'vGqP&`!n B ADqo7>4"Ɯ߰!>σ)OG=1>(jN(h ʅw13Lrvh#mvpj@{懙}y=r;xoy׃̓1i6^~r֖[\E'rھg5٧wdy#9\PmAҫT:lBb#7TiS[ V;=G|*;iOzzʊ[=K#7m!8<N*{l4`/Qߛ=҆J`4O.#%8N㺔W&iqٲNҼu_< ;kce<(OM^΃=)BBhikEf@#%|׌ O#7IQxqL#%ϓ^Ɗ:F4o470xf1oQoZ8(ĩµ#%bjd f~.Lx7eP'*K#%cjɤE" .,P3EJ*0HnL?V@(r 蚁#7QVdBDl>C[\2KofB}Wϝ˕PmO`~Q)߼ئ#9=Z$@ h,D#?cչ):ϔVK~THc͏l4Jd3~־Kr\7Hd,Ԙ.֞]IWzϻ9Cz>=y#9#0~@%dEpKHjH O_S LQK H0#(!%Ы}Hd@C@Ԣ _Ju:NP+9udZ=w|<)!#%&B^ȅ#Kw!H4nOIʸgg!i8#9{(ͤ]\gw[uu#%b9MFl;aYi'ipH=pzH|_`T\h4Tp#93ؼ=U*hpa2Kdˌ.S77T]87Gd#3|g>jB5H0B$5TNϣ23#%܅=t͆,ӳ/TdEmڤ#7Ta6d$"=Pzv!ToQ]!x!d"4ݺ%$!?<>A . 2Jx+u~Ķk:0 E",gdKh^XE45$>~?pW~f?FM#%/PCaK?Y#9= GQ.~tԄQ_6U(EZ("¿y=ڠebS+`|QX 1eQTUkݰ0B b".NG^^&@R t$'zX,x#%q:3O4@?_/݀}smB {zBl!~6KDT"gOi%<IO) <r{U;mZFUJ/@RAK GP jyw9!1N2\]#9Bh(ʦ_o#7Nq(1G>śh pCO3%J-EY)@$jK-݂9Q$"%QlFe >f{fO)u[z _JL;l/}*U)UZ:(qRڔ6mӸAftmm#%yO"vyz g=ΩWO;pPX6$U#%pͩvp;<| =AĻ YTKHs/G.Sqj")e ?|<~[~KI}?@saB敧ů ]dCC^c(7t-/L#(bT=aއ46\}1dCX;@8QKSQbv~fH`SL#^3o"C|rS)yʯ/ #73*үg믃WxT:X-N^ɰl"_3P#q$dBPT%d;evP|SjsxKv^8#I3{f0VkֵgީJ@s{OPFEC6nd7uiKgoQp Ҳ勼TnІ=0oƠ=S/]e-sRo9L*B@`(J4- eem#9^].#7;mZYN#~zэjͿޓj~A3/K6Waj1Hi5ⴎX@=V /XGg{&[kw{o, sc1>F\hUcr#*u\0]ϷE~}>>vy9s/0[ƜNԽ3g`p%V&@"ulV Zg󲹩#%/@}e1ףgBՒZErqg_:^-](iq8hwUXIJ\ѻ'\4ݐrb)'o2Q^lטoIcrI\gYwV7]>OA(= TE#%l r TAZ?:k4-FH?X_ܢz=UEo7^>x PĠ C#%OBZbh&DtwLao2m| iBf9s$g|07X;{0l? AL && 6f\b5]m5 =h3g#9YC4΃f0L_33T}gƘ_hԓD?V(3~'WspPY08vK&lu8\u%{$$ۂfr-C&J$E?g ?^Vx|4B ?Cw_85v@8EJ^Q`S.ONVG~06Dexzo8g5'9FS3^ urHz5KoMgӔ ȆGFÑ#%9I9}? ,X!tǨpÅ! +ָzDUel7ςBRA2u,.td^i<nT2fC &js'61;GkthMLI"nb<Խn"QG`-K˷"ݩEhmB :>/ts#9!R׳V1XI"5eR7(0']G)"b70IstĐ1Wd1ɭ*d;m/~ 9Ar#%T$9ʧ~YП,=@Dg)ʜ>_w7Pm #% TE!zcBL g0<ײ7N#7#%ȴMf\I8\򨪰z]>it<>V{oA_Z FR8U2%4#7kk!Bq9i4-mlnIvFz=)OaR#7ef0pxBXNXW$ (8zC='h\u"Y3/e.=2NGaTG!t5s_՚)H]|x; 0ÁA k 4##9Q3K@Ep0v+ f"%^w<\xpgD<`j_S#7"E5NwӘu?T~5 }_BYvqk'bz/|Aє_ɪ#P<}?w_f;Qh20>^NUT-#7ՒqT(+!x)ZcljJ&MAZV$g TCj"/h)anI#$cB(["Muxd@ KD D*a`?v@VT +C}*"-]vl?hh8a)yn #%*>*<'wpw/"+VGXt,|Bd ֖mZ6p@`N+V88ȡ^Vڱ CиQz7 =RT֠dƅ "v#%KO `4\#;K%gI7#Hqy 4#i5ҶEy=^:w[w||^6߷]_H/#%3Y()+ۻm.}oX=#7[\Xl`H.>ȶm@Y7X^V<*o9B#9n3#7m qd20`ԏR\tq"@Ҽ"#7^qۦaLü58eq#9%K;{d t֯;!g6#7rt7wWUvSkSb! u5!)`D@>aAGZC(/CC2wI΍qD3Np㨫ܜGQQpq3#7b̳(#7xgD%P6JcMxMm i6fe#9/NeRe,v##hm"-#90b=tУcU%/#9K¤h%Zo)8WB|3ji.B"F4bJY($czev{2d5!f #75",GJOizn9 YB,Ά%@oKʆCJ*^#%ȢH]i 302HbpMX梜G"|#7'Ld#Cij#7.R1yU474 |23f5Q8y#%6#dـcb`WSPHRS RR@hujl!Ȳd; {X5>:B%v#7zh {:;܋r5lږaVC9ڇBKШB/)ǺZT I-ˋBeOWd#%a"x!h(<66l tV!5JnBfa5 cPKr_)*$>=@9d}ɟab؍$$|+꥙B)2K&kNyٜ1y͚̓vrsUӟf%Үg hBu!$~O@K̨WBH9"T>+#ӧw5kg_ll2HCEei),òt5^1cuQ,TtmDR={G@J#F@EHXQQ>kӊyw#9\Hݿ""Gf|!ҵ}v踻BWCQkȅ"F@qVQw1%Py!g +L߷؇Ґ$Ųd)E+{Ifm(־u~!U0ϯ}x`l@nrW)O#UbK.B*݃@TI )#94zl3_I$L5H҈{TG|,}+/sB;58dTJBfS* `G&I QJ[Kh:l5=#9_u6kuvo6])ȉ;r̀<vЌ֔-k鯚QWI2kwz/#%Dzrktd(m*R!f 0f["-׫9YQ0]2=#7NQTa! 3%X)40#7Ƃ`#7[6_TR>ݵM־܎:ӎǼ0zB("#%!&:LȡQ B EhI@$U`Q9b1\$[,0_cD,$qh#>@0 [@`šF#7&3@X 0}|I炇Ty$PפE^$Ž]zm^Mv&7wj Kbە7J;W7*J%chr'#7VA*dZ?ԪрJ UT( f>H#7,lq<$OPV=fA|o' EnEAtVjƝm!¼pkH,eD3K~?|wo#%FqYvOFB)eQ#%H$#9VFٮshdPdMՙ, u:4RuCߨED?y&^*b?QOK8;MLd#9lyW8&ʘ3 'H*BP’}#%t0SK#9K?@eh{(-aY}^Cn=^[@#7-UҖmmXr#%>jwol pEKWwJO#7* [u6A^p6dM[q\/wI ٸ|ړ}M*A5jh8FS4C3#%aD!*TͦWP=){A (6 ?$FI"X$X`:+W̤ꍋI"o-;-G*'x-6&q&0].2d$忍q>BFSD| *#97R[ 9#%|u#%T4A"\pɈ0[_!΋8ʑT]IR C^Ud"hk,k$+W6*LRQledjIs"9g,n^0$QbE G!#7<?x1ntG i 8DZ'A^NyxaH>h ?(<}vC!??/g xQ!<dwש<[U y"yL ;L!k?#<|{`i1费(eMSl~ D\冁KC WgbnqU #7g?eRi'yKKў{:NS]dId!švGJmzӴƏRsu%\1Zs{3h2P;Hy;޷>#7w/]}<->V#9|>/f\ g#%R0r!>t+KN5o y(>=]? {QAWo_P@ #%Ch`2cy4F M&Ggy[z#9\Dd‹Mw{ŌaJHN82+GxzǡSC}]4RVT&cTXއ[Wn#%SAd!`T-%J|ʎ J{)ʳ#%-=?4(Q*}>ز"A|v,Rkź%#%+ךv8-v28C8r@F^n!@횾Dhp|jκv#9/Z`~9XK 2F idI>eY{@8nk1ݷ|Gc#9۷DŽZAӌNFpB@a\L-GoLS]#Qk 2xˑj/_:-mf @#7DrcŖ !#9%)^-q[Ɩ, Ѹ(\ηj |]ҡ?ev탵qms;uR#9b9A)ڗ+ja:#%m`ar?B`[,ETӁ.kO1$>Z'+9/Y`>g3מhaP79d$#7O:Ps:?mfU#%nq۷V3d1}bsgsbX:#7RhDwm 5qƽtBD#p!!lpnXzͯNӠ_SȡP(2nrS"-UJ xxz1ޘ(C/E'FBǃ0Q]\e44ZĆbc`%m-Җ{Yg:q3m~f&d]SgN,/t=64P-(*ǧujjF2#ב"N(DﮗJ II=YkYj?@A` Ae'ou{k.ڜT#=(t/%BT@\, -6hEgu-u5ߓ˖&X&0+Tj5 D"Q͌>]7a&'StF-&,3mEd$17;Qk#9lܘҳ߷}DZu%* J%B$kLPT@({C&l<qD0Q /R$QB9%t'}qqêLcфk` 6Ι]YHA@GC0jCR9&tv`k͸? ]>\ zv JR CjW#jDL%D%%^\N]檻RH`R%h5KBCϕ ,vڊ\:L`j5@"Hvi#<7erskN =p!1:@JlYKr/с{a^)y6^#9ZZcxft:b bkD$#%;+~yehg컓v2OAڌ3THT܄ FF;gk sd2V 0@l]Y2ohֶƌx! waBA#9Dp}"[i7P>g.H6~<tFtFɽ6Lޓ>޼"u,#7gRAtI#7bs1daAkpBm.4a*PF0 CLr3!p@sAL`d@۵ѤQl 0#%K dNoskX@ύ܆CHI3]eED"ЬlV#9MMo.#*H)FQ޲4a!  \YZ'[x3#7Bg#9KYaKaG[3A`<[rYM[.W_d.,0ny~,pIၟ&I < INmH- dh2#^=(ĞQgb_:ë̒--a057e iiB6Ƌh#%ME#7p"VyME8Rk :a&I+sa%P=F-4+*b68ӸX4g7k:8T0 (社Jh|,сĐ#93zRk g7Kв1v-slT-8&uQQx_,m.V<`:!A텷k@KZ|'QHtHi95~['{G#%i'eGiSPvv6n! SFiimBPkj;ptܧX$кqE+[:5p2g@:wCGo#;kPfo6v$?kI:OxlxSZTc,#;gCHMv8lR35v-)qy^|<$32zBh&Urg@dcgCɆv4o^"'[kP0-= r>Z&cDCod̳.C,0iv`)N6F kGAcW0QǸaPE6UKͩr!v1Sz2VoQ ;d3$! Ed3u77g)Jeb^̦)zKC-T.5u-8) m|IadLL"pE&.Hg'2^ ͈8ESšhި!j HA ̧B:*rn4Dǥ:YT;'v`.\ A8!#xJ3T9n]L(x132C%39'#/k3) yNÙ'vܺ;a5,ffHkMsYmSFٌz6׏w_/eF9=Hg)NLĞ311IzsO#7򕭊i:N3W{UXhKjh.Xr k7 6mA} ]dNXpog%+71z0 V^(ҦNmfAQ6Ny5n;>a_H0a]LL@چhk3& ڒ źG$L)e|HÁ(I @3s96 ['#9#(riX{6w(q'#9p. ԯc^kL%p[CN6ٶfzp.Į+<D\ڮV(W^^%ZN $̀doj:FۑhPi39\,4xXm[#9@4JLٝ-4!wXS#9bZimO#9(.ܭLLZ#v(yQ9HGp #%a9n'h<Z9deMD!$^1^"&cxejd7Փ) ID@J#9fJE˺h隃bJU ͜1m"!h`CT j)!tӓ%[Ja\36#90t8 Z  eѐ%qlݐS8SFp8\n0:KѢ ̠l th7jP%8eN,}e̹vC!#7"cl)zvȕp6 }@㣘/ FrznKd99hX1!#7#%RHonжĶbTVI[8mPF8q8"v}v-+X4RɛN[tR W" 9DϞAWɄ˞*4#7p9e{W|a=ngѱ,C&@LM!9\BlSz&S1waM0#%Je #70\ V4 DCsT*М9޿--Th8o%Ƃ"; Jf1[NHAXu7LsX2uK 0ZSAN#7omWdr"lyXI0`m@XHĈYM5f>L6.-HurV"F8\#%39\XdvQ8ʌW;eT.IG335Ek2B8s7ѯL,qyXk4V:&D4"-Tp"a8&Ej4jT!mz,ZD:#7I47#7b#7 %,#7ۚ. &ġ0&\Iĺ 9Ӵjaaupd`ݩA9946:AXF$#@JEaOO$RsW7@;E5(\>AxǚaA6Ҁ)ٮ4mKӑ1@#7XU~ig5b#td;$*,P i#9H OfZslcn*q RImRaϲKe -VOpNSf$JJk#7Mt|L(aTprז^٢B/kT:1i7k lcPTf"1er:yLNv]\@C׵4tM(D#9D"<!;l(n8ӹZkU",fx( *-#%<&Bp6} Q1F:uט!%|} j!*'b QTv"T?DBT(*n.nnۻ\+?CTAˬxR$"L]Qމ'1&g7]C >† EQDWŽ{~?(}N%?fA]̾J7ȇ4jfMĵu瘶xL{ѯ[1dHbQbޠU)Q8Yф#!p\o GpYJAJG#9 D4o/ rx"]T22&{mL{E$b6ŋF֋h1hleQDv"HDsf=ȪWbv.Id &8|E(`v"=]Ib1#i#%.ުhd*jQA!`2pN{v;##72x#9-}Nx;JD~QhTD~Xtz #%*m-`+]Rpԩ_V&01ցYT{^JoRЄLcF̥,؁-!i5fD kMqaPMUڀw(X'9C>>nFcWúwZᲚJF!3Iۻ^gV W0M|:iovm1NعDAF!Pz)p%8ͰDXBnPBM58=a !6hAL⤀"g!?O<=qk[t4.vM-l7;VCqA솁a`lX0J1.D.ESWs 1!6#%ZG0Ĩ(MliAapu@U $ #%ãk&{(-"Hkϯpqtd&|[mE4AB}ˊi/ X|GR~D%Ì*q9]_90)C@zB#%Dt8^+jED&V魷K5TZAHDP< sB~l1_Zwr~NMTj/?VrRo4oXQcmVV|OF˺ä(wGtS/&֔qYk4k8.q9ee(fv5,Q* \4]2@#R${QATwJfuڞtcnH5,U:RmT!pF#7c9:ѹ6cʳ4ѴhmD1m;'tVt4υc/v7 c0j]U1U>#zha2XۚMp9o%YܺޑbxL غÁŹGB/T#73#7ѲlYlj L'yIa(06nb$5!H "LV#%AsH!F{seic"IHw \Pj@hZh`#96h$I"D# 75 :%•3aHjȂD`RJ#7;1O誏XAA²1?8Ɖ:WR]b)ZJ#pR2wbmY]m;d7-zQ2Ӵuf%Dŕj+ѨmB̧2R$q(5 aj@]vjvꤐM֛")kZi*ڧ|QDX,T**\QT~66Fw1f0DQ8@bQ ,@waP{2.uw,#76l6#9Rcb*DR("~wc]oͭG;o5<4#9IBw`D-6dڊT[IDJII&涹P&5mk4mJlcR"e2R,ځJ3.ޗf&f4ĕUmSDVF5tڄN^v4jb(2#9eҌڔդb-4m*}jWJ^dQjUU"IR[m-$Ԣ%4ҳe Kl*ogm1G5-W^wɡuf+jcJbm&6DJa#>.?ɫ4*JtbG8 twWM# 7Bz)$ES(>"!=T @ ٖ,DF2Ɖ!&TtKMhѶF#%$؞XLjEE6i[)b5vk4)Wn^SUҭ•BELíE4;79)Mj*aaK#KL%jRilͬlM))naB)j+6J D4Bl43FLQ#7YE661h XԚmJ[)R)ԖIi"QK !BIFL%2M2jY6j"ie+I,mK5&-)6ڒkծe6RK{ͭE!"*XUsZjZU֑5jjmjƶgq=!ݛ̮Q1.T|8̷㙞͘؀iw55&s3؝#9 sB#9eNSKy##xTD_#9#%>8ݒza;`E]~^Vκų@f <` ?M򞵥ߙإ<*ROC |/?a#7\x õ l$"K #94hC#9V՜TF Q ! zz8Q//B~0G7Gd]\P;+շH&F/OƓfE3Bvkg!璩4fK\e\ ؂M#7ȴwX`PRUv6O½ya $>j )i%[c=-EFa,Ճ$j@A[~#cղ8o`#7XȦ4C gLX̵x!88̽mN#%i#%Bp#%#%[cj*J#Y0 )"#7dNȹ\y\PӏO۲1VI` 'Z`Qa&\eo0(cqVlcKgO-ak3x%?_MHtvvO{cwXv/PD4<]g 0wy3 \GLegvXMLK#7DQV8#%_/\ڭkX }A` ôD,n۵]'e_k_f&"}y}C$@" @t!ZYE`84iZ*4 #9,gfy98 *Sy lQIKU Bª9{`*IhW#qL{Ѫ#1{oCh#%O HCh=DnqH# ı>o'eMDUHh9;%.֑,=^.D #7+lZ=޼ڋJ#9hzzJJmc_ ^ZJxܧ^N-r`4"FiZR%#%iF@6[LrHl֨ )!G}fC-?_DJ$;b'jּ }G&̉$(ENIEA!d<@PqbӖjlj=a˟ DȄ#넀rt6B/19(|/"\ v=QI{E* DQYXDQQIflYx@IH,*L;Ѧȁ{ןr0~73!]m`ZD<#$cYB{.u[9Ru]cڿ߹yo?=IJ:ÓZnrk@IFEI8 C oĶOrdCHa]Z]J!;O>,/XxnԲ@K,0K2G.V)'mnJ-ˁG0*nD+wae-d~uE:oR ?SUW!LJ01@**Q3 ANRd`S#9XL2Tha B⁆UZRg㰫mExb#9.Uta"KM6l9Xn$pK0Vt5Td#9@#9Kx%2,ރ#%̶6҄qIl`EPX(JII*Ef-ʞv6[ͫkbEnfJq 0`%&7ż{-E#o@#9!hٶFX1#eb#ŨƖكZeIHX!Ba2X<0"+ aިYV`5#9m*8 FұYkJbLojuHmb#7էX,f0F'Fu"3Ya#M#7jƎCH|1pވ8ֱJvxTP uN4LXl'`D%Bel4a|XΩaJS2۫*6i041KHQ(&iC#%O>/뤦#%?_fVszm㣢1E;Poxq}!(!"\zшM鏁UIܼƥM%E1Ls6ƱN]M][dQ0Gd+#%I GO"ôCaQ!m2g7=  DxUhV.dos44<QV [0I#9 bFR .oyjJ] -|xYaDf*RIr\n[yK&&MK6xޚcZY[%Id*mzk۩/ȫؑ!zJ`)""u#71)&U֠DLf @#9UH=Vc::!d\XPsf Q#9WC-FKD^``b."#f"Qk\ʕ#%n;I ["rjd !Uf؛Lph^$[ _$KfGjq88Jv %RY^,#7"f׎un4ҔbMM5Y&Tb5MfOjCN^FIYmѕoM*$"*qYdI_[;޵J(*H9x<\DRbM$MʰG41,Q~׹bN,Ԇ3T^2JkXYBA:#9sEη0ܚqdqJ,#%z7{QHQlFM3iURj-FI׼$MWeUjf6 dZlm6mMm20m4Z+mb$r@~0#%~φl[!6 ErDdPN-Zk#7b# !Y<;8#7mB2*z$$!Œ~@>T QHhU#9F4K*Zddcd~bL dz(q0^ gR*8']#%NqE#9"#%t#%߱'D7`g>hD[DBG$ Px.9c#v5`ŠlHhGxy84C|#%op&BaHvzχ52kN8 XD71 ^<"|ỏ4$^!G /zH@3sbk75@NOg`&#%Alk]%3 \jiAB aqmpl|#7讵#9*]fi$4I#j`)4uv 1\jyo:6|4{PApiF(tUxE>r|мamFiA8(Fb?Ø/&@|eM H(%YJHR#73Ԩ4&(P#HC`6 nOމKo.H]QdhDPUQQ. ɍLxEC^* 7/[02%ـG95@c[?"z"8$nYFZ%Ha{êTyG#7e2~@#_t eZԯ7FbK5mƋdP l8z~ݚlU!bNON%)JaGsY~ b*IBZvA1ݚfkJ c#97c$FG=R6:VacgUAt& \gipt:xur!."eVȺHatK,J7g(0k5fzZ.kӯ`$Nj6^օ;kEd L7EI{CX3|?6b㬪,)1θhvgǦ#7,J@YT`B#%JEzun6X"u6Q#9=) @7qϹU#E8N[P ygmC#wr" BE4!!㗊DFzcBՍ 4vE@m姢hlA&TJ#9;zRt1{#%}Lw :0)1@b@6DjLW^y[vJI.DF>%.PD %`BxP^Oդu@* pF CM_*XR02#9R,PMҴ..Yzh޺.c#9uhWX46Jiqh,ٍ7k1[<[w/k=tr+ҋB&xMj#95YFY+O!-0Y# oXK)VV8kB_@#7N$zL˅Ġm"RɈf "3*wNJvϼؒ,GiTAt ?L~m]#7&)N]ʶwHV%d3yQV r)]Nm[{#7oĵm4TVMYJIVƩUttwm\U-,T~< pcACc~aI#% =PT>3`s)N]g>\ WXeNƏə3"d1 x>ejOq#92[}#9,iYxӽ=K#7FևhL m}8K7}a`3rZR7<gfo,1{n.8$#W, gv#9l&vLdlB&G##bP[}?mlڨaqpG]mH#7/lxQrZ'1vu@jNUNOv]"co7f6VWx ȧM`#9}t'xf˵LD20EEiL(6Yv?(NɑNt֯[!unu:gi p8{z;;UZ0l2L-mXX#98p ˅Ŧqp7*c؄ v*R%MzA `ǭCd$7:IIEeKT8RcB@F#w#Ij"hkš̴άW5F;ء,wx \(zf}]Ǿ0 PɲEjՒJ$Îuͻ-۬44ӳM&-+mlXf^.^]Uo,R#%V#9Ȉ(wYC`Nø1xdm>6#7jǦ3svh+ 2 5ZC 7Dž`e'G\{ڝiFHH#9#%2`0 4qf)H>hnm&xz\-4 b[L 2' 5 Ѣ4j͇!VX"139!\|Ȍ &H)׏;p&27h&WCxP6u2ed!egrWp(=%#9{8)g&xl|QrHOqk٦*`Q*n3{~m6#7bV'Y¬bA'Cx;?t&{R6%|-RH#%]D& VxU}eưֿNf5 .DsA(e8۱d 8$LS}[E#7#9Jkd Zv+EK;3]/AfaRZu>2j kZeJ+u?#7WM%Ln#9 T)*&Fn4}x =AM4\ѸR=v bÄ#sƱdMQUkFQ,nmtB-IѵmH  D*2#"#7AڦnJqwXD`Uz,.I+۴uvgDTd{,St3$BDI3ӱ[.k#%ۻt&?].}ǽ?vy#7k2m`*bsb&1 `Gb 79q/ͮp Yv~rA q=#9:p%PqX78#9;;vNi?n\0 W?Eɺ8q9;\`TP㘼xlZR*00mА|l^B.Vt:0VSfRЛ&nҋmi{ٍ(Mx1i_8v3IlneϽJ#XSU%Hj64Xm4#7qFVVVAV+Ic^yݲ^z+B)Aa#723cLchdD1-Y^\FFen)) ̐`ّ;֋4zb{YN~52t,Awpv{DdҖUkDɭ[ldY@8?#%T(alMI=̪lczINy hABcDDJZmMh˪  10c(BbQ6@9=ofv5#( 53TA?#7 6߆#7_]>m!|fg&շNͥE+BSlZV]w.cƲ. *~ۨ6̹L+Lv.~KPzWXW]Yd^JuuM+'t?v;Fv,|[lᙛyls s:@(2~JVW+H {,Ԯ_œ塑([ H@v@! [+և4#91yІTJaUtbU~ Dy?YvoM:Ntn[.lx$bzmn7U +4ѽ5f>'BCϼ0|@!ڹmnQj1Cv ]AKI$BEV@4<#7S=y%t#7Y۪XLvV1uI:O,ƃwpjꃗF\ָiG]/!F%(f́*I#9l'YJ,QLq4Kr67q˯p}7g) pܟ:a#%oQ`FI @跮jտj-[?,T$XAC9aQ0E $I04|O1Ī+5ZZd[:rYц-x%YbB,*-ivUkU%w[jjy^fB2%-Y6upHKDBcChcX,kz͵KoSּ5f1. Hd@#* ò>304zaDrT!V)D>GiMoP#7S8#%jUͦ4ڦl>4"f#{&ZdQkl*UWɁj}<@a4)w6Z20\T4uSJCU}qFF#%v\Mڿ#5I6z#7^:zt7~"} 203>M~~4cI ,/ شZӗƚmVJ`^Oi]mqI'xU):?E%/`!ԑg[=nI%I#7IJ-#9]„"mCa#7(b Z@W\vmrI5rwb+Z$%DRmьb*3UV`ڶ<~WoMW7mxڒX(32fUxݗ`Ʒ(*$^쪰r- H.h4ipux)yc]̚3$R ta%]V I#9#%FVpr ~Hےg'XI~h\ ƎTGQQ!:Hxy/ϮE#9b"n#9lv(B:10HhH,ENE >#%l5$c$`BH#%j`QYK@#%,HT`#%=AO 9@$Xā0)"Yʳ1U";ju8!!+u USw>ܫ9#7䔒B#7b'#93@7_&]|//;z62mWsK#:OK;vӶNd-0؄N0#7X.,\a|;8F^8(IWyUHZ2ek3qzd Hg]NG+ljEԭZ~nS-Qʑ!CKAC!QK͋n#700ynO8\@<F#9*$$F$>@sg"9> !cj=,5/:7P)iT '_E2ELAD:$;$&Xќ CL -+CnGBHpᤣRu4#9BG0O|w5߼5/YS|J5+as6$L09s#4\)Vc0cFޝ {rk!}oQj ϲ:Tw ćRB% 2Nh*/l j117zͳ`_f3AhcM'>v#9rf7+>1M^b7.0:0n#7Zx3=Wz4Cԃk=i1KB~3.5O\G٣t`F(D1#*T,D%+>u#7ÑM]l>z;^`К@Pm4*9gwg^3T8 z;E#7x!mL#%Ғa^$& 2eg=KE-"10C=))#9,0!#7CNGyG=vѢojx!BPT|R=.5=e"kNMoZX& J>ظ`BH%ER׎KdjjsWjoW~#9o#tYQz+gr(eOݠ&+OD<&݌hHNbQ2ؠdY4{9hCIMb J41˙~:yVkP`F*OH@O{f6mv?Eð;˸0/'*VPVq- L5#7SB@ΔNTUI?~I\t:Z:v |{!1a)@h#999sɱf*2ͣ(3ĥA Og/$F4#9#b(Z5n<ᝍAf]W-;`ѽ֗cŵU8w>f;`ylXFWm>u@m&aL]@{A>Cզ2R)yuJo5I!p&_;⏴EPvOϥ$#7Rg.uW2j6YzF) R]uv*YFI&R颍d  +bF ␨ ^*V"K,i(e,pVj+bIh&4+L$ہ.46l1FzϘ|1։ r,;GZZ!(R̎)%㓸m7"~-De i\2T-|YT)EfL20<'5R_uR A,Z)t*OMѮ)#7ayQ]:f́01ΘApFM%^}C+Qj[,HdD< ڤ 8u [UD#%4B6&Φ%Bb\cI"6$2*LHؘ@/i,܀HB.#%6$POvEBZeUKG]+%<Gvw)h_nPmQmjYlI|nڅ)C@L+iV'ŭ!ە#O]m̸E@@B$bRS@KK}ER]}Мt\ #9Y>>NyyC\S&X( AŽD7dlLd#WZ^](sԾ!F*e1;Z2kE6#97ɋ C c[(#͉'S6`F, AFu0l3~–<ɚ{6̊L6-zU:\k׹V`HIf\hVDxNFLw -d/b69:%zP6}傩$|#9>^&Jaࡆ1'BJω#%p]~]#7`zr*b58SZ>}{A))t ](ve@[ 픒rp8Z -7Ʊ?ow]5rAN~O1ω@!50$%ABB'D2sB! D. \rjޫɰ#%Njc%@S{[6[lr(@>U:[YZ!q'LD a>_D:5I,|;B)2 H" HULcӝhJJ%U%!D"#%\**5,#%A;D@DW%Y")D H$V'VDu IowQwDjYFlIV*X4T]?g|=P=rjL4 P7/U*z/kJ#9)x: ̿9"$;F䬛邾ڐ:P=6/#%H*e05յ)Z ͒Ie#9 Q$eI%(2LhMQkbQiYZeRѩ6!&~F}U mF#7A+"e mFeYH7$y-+2#7WX`K=0&3 "*H(ǐ1$Mlr jʇDP#9-00F%MK;neUkT `ܾB #9X?r\ J@mPwN֟]^!AbHAlbc~p?%4'uOW[oߏ#9nREI9)AI"(9ȢHznU2^*E0_.sΕKuh_Uzt"|lHH[ee'4m_=VFB;A+̪!p(p4ezq^vM7gWY` J$Pz}>P]C\RH=$a1gON;꩹YRͽMF:5I]_Xp^iN _/ٷPa9'GHY֓kg>qWo ᶭ?C#H@Hʭ2GhNjg++BCgKBH,Sg/"yUGa'u%XıK*BuQM'|J`MBlȶq*$/kƻ43ɚ%aSnP(sjh\'{KX?ѵMQQUܯ$m5D!aM]eq.EmG99zf+K~ggsmm9!y_-\l2a$3B8hҜ%r5#97]i2ʰzrp>y#rfdώQؘ5`m)#Cew6FYv3;T؆ Ehq+.W#9F`/.8N᫶Ef^aTkXJmwmcɯ]Z;s'&^I$SQsM&kd:\h< hTI(8EޜΟj8y۲rÉ+BB{5ڵ:$9<]{i@6 s0lzxu4‚ĺvM˨WᬎaoT]sw3]Nk|j]cR|Kh[6I3{mkh58Vg}s̴A<#9#%-W9ٻ򠲡|RV,pb#7"7g^G(͈룳{]+.Y#7Ƽs&L#7qb1_Qu֍frU#o! ПO#yDK0]h-y˴”oj(2vf2vף[=:XèlZ3#9z\3R MF2V]#%BJHA pshXVUF54gQUZX=@BҒpaC8ywv2:]#Xij#9[#7Q?)*F}=D9e4{ac]+to)'#9#7f3uDO3[zpCOZUJm(#|H[խcR>Ab: 0?7jRM+klq@\acR^_Rkw,߻ﳤև=큯#7MR_kX{gr=K$TΟyW#7~#7ccF6&HБ)>khdӊl#68J6[Q5B l4L*xc$i4( F6VB%T E*ZTT: *( "#9HUjHYLYAWH6`XQ%A4(TQD({Ў_#%ɀ[D}[hHBM#7zW~v*XB0#%Y#% Zfv"nGSoX1ibn P/i`nfkF:CO1h]2D`Fu1#9X`@.h'UFBC&jR"4#9q.k%,"E VУ#9dXaE,%Ecbi4CZ*=ئQuwL)$Y/O[>(%*(d $5ěRn] S5Q\abnF;b#TR!13a03(4BCqVj)\ Xe9MZ9eolBb0⠎0b,2UdmA6B$e|]ukig)%u䀸`6Im'7UJ0+)I P9K3.cuadK,7eԢcvӭuZ HN'a-4w]P \R46Q)22s J>͢e^6t+qEnUT(UAq)Hi4bjW[y2Yeva[ tC :6EJ5;[%\QFff6l*4^U("'9--CFF\j@Dm4GSU:3F)/#9+%dAhBFL"T[ b[cBfEXj:4ukDf339;5:wqNF,DyUVPC+o{SEV^f>2@cr-HuL{AV#pkKLrC)o̥08dZ6pVVB1<L֎5#9dLp6A 3֪R#9ebkw9*h4"##%肋4Bi #9[t#74RR`]f!tÌ|B$~zқJ۞UU3d>G{1J5ICh.]60bqӟ |fW7Wɂٌn|#n76Đ蔴`Cl5>&, r&jeqqxC&TS}PDՒ%MhU>0B:v-_oX6D_!M46)>E0e={BDDu6ÑqE I"`J;~o٤5maS >N ~9pe+sX4CE&!𾟓bU%Y p5+ zGc/iOȿLb}U&'J*.<; 9*;ҕiS-C<ۻ{56cE@TX>8IJYq$o<;*6hhlQ)!F#9lFdhpQ$PN*KCPK04 PmnEFjE5 jCs#%QڲlAM(΂nC<J#%S , sav{`LCpó78ڃdԟ<6Et#)٨5WM.$3 jҚu8,LO2V]u|BiYs(tR0\ڋcqNvR+ 2^w=7C-qd$lhw3)G%95[70k>϶$hM[ĩZwe8G;8k\@r2n&='N:hYvVfkn#%DoN4I EG@/JfkZ(ZfƉ|Sgb`+wp9"4,V"ىnNhn#7cJl,i#7*\]'m'a`P`,q@uX8ӊ#7k"B78aeF=$aT,ؠciQQIB$պ՚l42nfFdfT#7䛬$}2 m]sy~cN Z1 #€JaӧoߢlZl?=nYviU,3)#E$5[`Ƙ,V9l9]LۣI?|#/m2)r剦.N:tl SN^R ;k7MӳfSi/LSkQ~Zm5PFk ݓ]pb5-g`X*4S&uAlFL:BH@0.;#%\;vvKP$-#?tO 5vPm"AРУ h3׽q5v4UW#%h!u#%UΫgzc+#Y5=]797+VtEtrh#7 M~yܶI{L$,Yi,1SֆS"`-Y)BsLd;$IRk- ȑI4(7,!L)#$Pm=3$pc|h(IWJ;m'ٔ[]9RiƙF`ȫoR@۶RsNݙh6"#7B!lLbn/Zwdc#%FL(Eu6ux#7I}#7V\G#%m@ r\Q*rQK#9JD(%T)X:gE.Cr.",8`#7hE PSh#M( :%#7wU*evTTE#7#94u)S<-alA*PLT9@bXuq"PYF,6^O&Tиw#%y`ŠGaC-f].P4*Ȅ22k +Ei<:cڃj*"( kG8AÖǿ~Xh #%! &PEGNq<γO03tZ*_MtKKs> @ܒBH-o-Fᄏpmu )#aE뻺fDEY1G׻nڹ*ڈ 2˹E#9Ы )8s_u`/qɎ߮` VF8Q'vqH9\ A2z[v/ΔKE? ZE4ɓÓ.Ic$5&:F~A#LHjn"*`.SeUԋmμa4|:,#9#[3>at1`ab#%om@|=ORuev%ܒ"va\rMcq5kLY}h/$0I2Lɒ0(Q4v=2#78ё뒏l%_"QuSoZQ%i6ͶR$S}IlJjҬoZˍ"\/c@*)"#7* `"bj*IȢ\r8NAioZoȌH$ $ 'ڦ~XqV1U_7n2]PG=#7J/hv#7q::"QB(@аѰu0LG ēaXZ*R1T8#9V'`ģ[4(f1$MJƯ2/nh*!C]`xāޠPpΒ#%Ჴ"f{XZ]_*:7 GF졊yfyñr QE\ `Ā"boϖKRK U}*7ǹh"IHJ#91&!0?)vNP'!1#)_p2|4 X%끢jI%k9l9V`ReIGc/2\J7TPukQW+JTBJ97H8L %~OHܭ7y##S6ɩ26cXAmmf%4TcjVbqCH,Xy xZqr'45<:9t@;ϐ:]hXx;`# !>_]AQ܃]2 )x㞽PCw=>|e1#7<-A#%7EzjrlDaI##9#7ǝQ=O`Dq)\҉W *l#7c#.zamHܒh2ݶw#7_u$S4i * p6\F#9`ڭQ\A)A51(6?Vǃ8"d ;֧>ȲnF*ֆ +Xpm/`33[&2E'݋@lE!O#%zb#9+")@w )q؈$ALxIxNN`IJ(NvI0c"#%GAF%#4zJJpк ԝ$9LOvPAQ rH:JXkm?lC)Ӱ6" MHjϕ??Vݷn6HmIv}k3_`[(yq_`:npEԘn2EʼF0^wC&3#QbF.#%0baS 7rK4dd47s\#9Jms-A*BTEl)DiS$Sh7l)E#7_I_OǙaJG4H)Wb5M)!p_>Or(i"0H4eT[FoU}ŒښcTViR~;S$o52eBR5OX">DV›`QW[֛Ȧƨ[f1QebQlRIzͶ#% )e@1nJyݏ.$#9]CQOi΂b!'  = ڋ B#7pD3g4,Be#%=@#7wM`J2S>dG-3;$D/V؄dX d qZ.#%YB>i VTػ[ SZZM5S;$dՈ@PTͷn쒹N#%#9xʔל$B/˵-uCdi|65W4&cPuL*5XV6hɽ3ΰ0הT`fRer7xiopȇs3EIs| u#J&|hqFd8yI n(7ةdS@]EBF@0mJB[eb6!y1ӡݭVX^4A@с,Gb #JK(1;.Lqh+#7G[8fdI1azY/"'-.(8?qQ6ݢuefSF-*NWwZE[nJyٙU7mE\A%,ڼ[i6T2$4Bh"%NJU#7#98 TS)3ge4SRezV[ή1YawX}$>QS3cpU7T!@yr΅(#7qR1ɫ9Dc+c)*b"m_*j#7RO卬vyI]B8Hۏ& h{(k4C25#7!Z^4i0K?Bf3[%QhdtGQ9tCB0ῒacf1+BG_pzÐ ( :6w pW5lr-@=]m#12p2@R}E&FKeP* zE`7SgT!Eml(qx 0 }~6F2DXJ*OeVao#9TMe2iF~12`Jć;n&lqp\{1fEhd| |EdjUc'FHy~2Jr!NKnS.4$wջo4 !^g[8[o2|b̈ݼ0HBJSOIãbRtWDh~-tyec.#%hA#7IiAFTT#ZY[xcnVhv#9[`[.Uu'eC'`s0j+%(6&>0OD]’ !\>C|pco3WeI3!~O}=4T"ZJqLKX #%e s\6,0@GYb2;,PH$`B9 9BkdDL6)&g _ݲ`'nEd##%0e"&蒙LAQ蚉CPmej5fє"cB H M(R#%@8 ~MV"h K)+#%C=?V?Z~Q,ױ6!Y?RU|GrnUS"(~*uN&:ۢ]thDQn. xtAh#9g,r.[IղN;@l[#7[#9c;<J^PYRMJ9cV̐T4,$>,5>VU/644~/qh~0\`jqeEx֒ onbmʨ񶹵^VXIch66+WxwZז mÒ܊#! #7#7!Bi,9IN7PW|7j%!5"S>-h%k$ ihAtE#7d 2o9[}<>Q#p?`MĄY#%Y5ZZ?iT,=ޖL>OngK<ߧ;O~|iJ/-9Q{ǫ|OOO9dq9C8xD8%une^-.]DݫGoUf'uĽSP\}һxULp9SCttYX+X߇hTK^V9 90֙KZx!?j H6!ŸzSO93w@e8)E/Bڠ[#JW<:r3W#%w$S c #<== diff --git a/waftools/zcm-gen.py b/waftools/zcm-gen.py index 894a3afc1..e6c030dfd 100644 --- a/waftools/zcm-gen.py +++ b/waftools/zcm-gen.py @@ -4,6 +4,7 @@ import os import waflib from waflib import Task +from waflib import Utils from waflib.Errors import WafError from waflib.TaskGen import extension from waflib.Configure import conf @@ -270,7 +271,7 @@ def zcmgen(ctx, **kw): if 'cpp' in lang: ctx(target = uselib_name + '_cpp', rule = 'touch ${TGT}', - use = ['zcm', uselib_name + '_genfiles'], + use = ['zcm', genfiles_name], export_includes = inc) if not building: @@ -290,7 +291,7 @@ def zcmgen(ctx, **kw): if 'c_stlib' in lang: ctx.stlib(name = uselib_name + '_c_stlib', target = uselib_name, - use = ['default', 'zcm', uselib_name + '_genfiles'], + use = ['default', 'zcm', genfiles_name], includes = inc, export_includes = inc, source = csrc) @@ -298,7 +299,7 @@ def zcmgen(ctx, **kw): if 'c_shlib' in lang: ctx.shlib(name = uselib_name + '_c_shlib', target = uselib_name, - use = ['default', 'zcm', uselib_name + '_genfiles'], + use = ['default', 'zcm', genfiles_name], includes = inc, export_includes = inc, source = csrc) @@ -306,7 +307,7 @@ def zcmgen(ctx, **kw): if 'java' in lang: ctx(name = uselib_name + '_java', features = 'javac jar', - use = ['zcmjar', genfiles_name, uselib_name + '_genfiles'], + use = ['zcmjar', genfiles_name], srcdir = ctx.path.find_or_declare('java/' + javapkg.split('.')[0]), outdir = 'java/classes', # path to output (for .class) basedir = 'java/classes', # basedir for jar @@ -315,12 +316,12 @@ def zcmgen(ctx, **kw): if 'python' in lang: ctx(target = uselib_name + '_python', rule = 'touch ${TGT}', - use = ['zcm', uselib_name + '_genfiles']) + use = ['zcm', genfiles_name]) if 'julia' in lang: ctx(target = uselib_name + '_julia', rule = 'touch ${TGT}', - use = ['zcm', uselib_name + '_genfiles']) + use = ['zcm', genfiles_name]) ctx(target = uselib_name, rule = 'touch ${TGT}', @@ -345,6 +346,18 @@ class zcmgen(Task.Task): ext_in = ['.zcm'] ext_out = ['.c', '.h', '.hpp', '.java', '.py', '.jl'] + def uid(self): + try: + return self.uid_ + except AttributeError: + m = Utils.md5(self.__class__.__name__.encode('utf-8')) + up = m.update + up(self.generator.pkgPrefix.encode('utf-8')) + for x in self.inputs + self.outputs: + up(x.abspath().encode('utf-8')) + self.uid_ = m.digest() + return self.uid_ + ## This method processes the inputs and determines the outputs that will be generated ## when the zcm-gen program is run def runnable_status(self): diff --git a/wscript b/wscript index 6d548ccfc..a7dfb8a08 100644 --- a/wscript +++ b/wscript @@ -239,6 +239,7 @@ def process_zcm_configure_options(ctx): def attempt_use_java(ctx): ctx.load('java') + ctx.env.JAVA_JVM_EXCLUDED = True ctx.check_jni_headers() return True diff --git a/zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.c b/zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.c new file mode 100644 index 000000000..96f20ad61 --- /dev/null +++ b/zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.c @@ -0,0 +1,311 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.h" +#include "zcm/transport/generic_serial_transport.h" +#include "zcm/zcm.h" + +typedef struct JavaSerialTransport JavaSerialTransport; +struct JavaSerialTransport { + JavaVM *jvm; + jobject javaObj; // Global reference to the Java object + zcm_trans_t *transport; + + // Method IDs for callbacks + jmethodID getNativeGetMethodID; + jmethodID getNativePutMethodID; +}; + +// Helper function to get native pointer field +static jfieldID getNativePtrField(JNIEnv *env, jobject self) +{ + jclass cls = (*env)->GetObjectClass(env, self); + return (*env)->GetFieldID(env, cls, "nativePtr", "J"); +} + +// Helper function to get native pointer +static JavaSerialTransport *getNativePtr(JNIEnv *env, jobject self) +{ + jfieldID fld = getNativePtrField(env, self); + jlong nativePtr = (*env)->GetLongField(env, self, fld); + return (JavaSerialTransport*)(intptr_t)nativePtr; +} + +// Helper function to set native pointer +static void setNativePtr(JNIEnv *env, jobject self, JavaSerialTransport *transport) +{ + jfieldID fld = getNativePtrField(env, self); + (*env)->SetLongField(env, self, fld, (jlong)(intptr_t)transport); +} + +// Timestamp function - uses system time +static uint64_t getSystemTimestamp(void *usr) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (uint64_t)tv.tv_sec * 1000000 + (uint64_t)tv.tv_usec; +} + +// Get function callback - calls Java nativeGet method +static size_t javaGetCallback(uint8_t* data, size_t nData, void* usr) +{ + JavaSerialTransport *jst = (JavaSerialTransport*)usr; + + JNIEnv *env = NULL; + bool isAttached = false; + + // Get JNI environment + int rc = (*jst->jvm)->GetEnv(jst->jvm, (void**)&env, JNI_VERSION_1_6); + if (rc == JNI_EDETACHED) { + if ((*jst->jvm)->AttachCurrentThread(jst->jvm, (void**)&env, NULL) != 0) { + fprintf(stderr, "ZCMGenericSerialTransport: Failed to attach thread for get callback\n"); + return 0; + } + isAttached = true; + } else if (rc != JNI_OK) { + fprintf(stderr, "ZCMGenericSerialTransport: Failed to get JNI environment for get callback\n"); + return 0; + } + + // Create direct byte buffer wrapping the native array + jobject directBuffer = (*env)->NewDirectByteBuffer(env, data, nData); + if (directBuffer == NULL) { + if (isAttached) (*jst->jvm)->DetachCurrentThread(jst->jvm); + return 0; + } + + // Call Java nativeGet method + // RRR (Bendes): Wrap the java gets and puts in a try/catch + jint bytesRead = (*env)->CallIntMethod(env, jst->javaObj, jst->getNativeGetMethodID, directBuffer, (jint)nData); + + // Validate return value + if (bytesRead < 0 || bytesRead > nData) { + bytesRead = 0; + } + + // Clean up + (*env)->DeleteLocalRef(env, directBuffer); + + if (isAttached) { + (*jst->jvm)->DetachCurrentThread(jst->jvm); + } + + return (size_t)bytesRead; +} + +// Put function callback - calls Java nativePut method +static size_t javaPutCallback(const uint8_t* data, size_t nData, void* usr) +{ + JavaSerialTransport *jst = (JavaSerialTransport*)usr; + + JNIEnv *env = NULL; + bool isAttached = false; + + // Get JNI environment + int rc = (*jst->jvm)->GetEnv(jst->jvm, (void**)&env, JNI_VERSION_1_6); + if (rc == JNI_EDETACHED) { + if ((*jst->jvm)->AttachCurrentThread(jst->jvm, (void**)&env, NULL) != 0) { + fprintf(stderr, "ZCMGenericSerialTransport: Failed to attach thread for put callback\n"); + return 0; + } + isAttached = true; + } else if (rc != JNI_OK) { + fprintf(stderr, "ZCMGenericSerialTransport: Failed to get JNI environment for put callback\n"); + return 0; + } + + // Create direct byte buffer wrapping the native array + jobject directBuffer = (*env)->NewDirectByteBuffer(env, (void*)data, nData); + if (directBuffer == NULL) { + if (isAttached) (*jst->jvm)->DetachCurrentThread(jst->jvm); + return 0; + } + + // Call Java nativePut method + jint bytesWritten = (*env)->CallIntMethod(env, jst->javaObj, jst->getNativePutMethodID, directBuffer, (jint)nData); + + // Check for exceptions + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + bytesWritten = 0; + } + + // Ensure valid return value + if (bytesWritten < 0 || bytesWritten > nData) { + bytesWritten = 0; + } + + // Clean up + (*env)->DeleteLocalRef(env, directBuffer); + + if (isAttached) { + (*jst->jvm)->DetachCurrentThread(jst->jvm); + } + + return (size_t)bytesWritten; +} + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: initializeNative + * Signature: (II)Z + */ +JNIEXPORT jboolean JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_initializeNative +(JNIEnv *env, jobject self, jint mtu, jint bufSize) +{ + // Allocate and initialize our structure + JavaSerialTransport *jst = calloc(1, sizeof(JavaSerialTransport)); + if (jst == NULL) { + return JNI_FALSE; + } + + // Get JavaVM + int rc = (*env)->GetJavaVM(env, &jst->jvm); + if (rc != 0) { + free(jst); + return JNI_FALSE; + } + + // Create global reference to the Java object + jst->javaObj = (*env)->NewGlobalRef(env, self); + if (jst->javaObj == NULL) { + free(jst); + return JNI_FALSE; + } + + // Get method IDs for the callback methods + jclass cls = (*env)->GetObjectClass(env, self); + jst->getNativeGetMethodID = (*env)->GetMethodID(env, cls, "nativeGet", "(Ljava/nio/ByteBuffer;I)I"); + jst->getNativePutMethodID = (*env)->GetMethodID(env, cls, "nativePut", "(Ljava/nio/ByteBuffer;I)I"); + + if (jst->getNativeGetMethodID == NULL || jst->getNativePutMethodID == NULL) { + (*env)->DeleteGlobalRef(env, jst->javaObj); + free(jst); + return JNI_FALSE; + } + + // Create the C transport + jst->transport = zcm_trans_generic_serial_create( + javaGetCallback, // get function + javaPutCallback, // put function + jst, // user data for get/put + getSystemTimestamp, // timestamp function + NULL, // user data for timestamp (not needed) + (size_t)mtu, // MTU + (size_t)bufSize // buffer size + ); + + if (jst->transport == NULL) { + (*env)->DeleteGlobalRef(env, jst->javaObj); + free(jst); + return JNI_FALSE; + } + + // Store the pointer in the Java object + setNativePtr(env, self, jst); + + return JNI_TRUE; +} + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: getTransportPtr + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_getTransportPtr +(JNIEnv *env, jobject self) +{ + JavaSerialTransport *jst = getNativePtr(env, self); + if (jst == NULL || jst->transport == NULL) { + return 0; + } + return (jlong)(intptr_t)jst->transport; +} + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: updateRx + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_updateRx +(JNIEnv *env, jobject self) +{ + JavaSerialTransport *jst = getNativePtr(env, self); + if (jst == NULL || jst->transport == NULL) { + return -1; + } + return serial_update_rx(jst->transport); +} + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: updateTx + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_updateTx +(JNIEnv *env, jobject self) +{ + JavaSerialTransport *jst = getNativePtr(env, self); + if (jst == NULL || jst->transport == NULL) { + return -1; + } + return serial_update_tx(jst->transport); +} + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: update + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_update +(JNIEnv *env, jobject self) +{ + JavaSerialTransport *jst = getNativePtr(env, self); + if (jst == NULL || jst->transport == NULL) { + return -1; + } + + // Update both RX and TX + int rxRet = serial_update_rx(jst->transport); + int txRet = serial_update_tx(jst->transport); + + // Return the first error, or success if both succeeded + return rxRet == ZCM_EOK ? txRet : rxRet; +} + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: destroy + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_destroy +(JNIEnv *env, jobject self) +{ + JavaSerialTransport *jst = getNativePtr(env, self); + if (jst == NULL) { + return; + } + + // Destroy the C transport + if (jst->transport != NULL) { + zcm_trans_generic_serial_destroy(jst->transport); + jst->transport = NULL; + } + + // Release the global reference + if (jst->javaObj != NULL) { + (*env)->DeleteGlobalRef(env, jst->javaObj); + jst->javaObj = NULL; + } + + // Free our structure + free(jst); + + // Clear the native pointer in the Java object + setNativePtr(env, self, NULL); +} diff --git a/zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.h b/zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.h new file mode 100644 index 000000000..7989695d0 --- /dev/null +++ b/zcm/java/jni/zcm_zcm_ZCMGenericSerialTransport.h @@ -0,0 +1,61 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class zcm_zcm_ZCMGenericSerialTransport */ + +#ifndef _Included_zcm_zcm_ZCMGenericSerialTransport +#define _Included_zcm_zcm_ZCMGenericSerialTransport +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: initializeNative + * Signature: (II)Z + */ +JNIEXPORT jboolean JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_initializeNative + (JNIEnv *, jobject, jint, jint); + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: getTransportPtr + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_getTransportPtr + (JNIEnv *, jobject); + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: updateRx + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_updateRx + (JNIEnv *, jobject); + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: updateTx + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_updateTx + (JNIEnv *, jobject); + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: update + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_update + (JNIEnv *, jobject); + +/* + * Class: zcm_zcm_ZCMGenericSerialTransport + * Method: destroy + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_zcm_zcm_ZCMGenericSerialTransport_destroy + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file diff --git a/zcm/java/jni/zcm_zcm_ZCMJNI.c b/zcm/java/jni/zcm_zcm_ZCMJNI.c index 7cc01a01d..d08280c68 100644 --- a/zcm/java/jni/zcm_zcm_ZCMJNI.c +++ b/zcm/java/jni/zcm_zcm_ZCMJNI.c @@ -75,9 +75,31 @@ JNIEXPORT jboolean JNICALL Java_zcm_zcm_ZCMJNI_initializeNative setNativePtr(env, self, I); - return I->zcm ? 1 : 0; + return I->zcm ? JNI_TRUE : JNI_FALSE; } +/* + * Class: zcm_zcm_ZCMJNI + * Method: initializeNativeFromTransport + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_zcm_zcm_ZCMJNI_initializeNativeFromTransport +(JNIEnv *env, jobject self, jlong transportPtr) +{ + Internal *I = calloc(1, sizeof(Internal)); + int rc = (*env)->GetJavaVM(env, &I->jvm); + assert(rc == 0); + + // Initialize ZCM from transport + zcm_trans_t* transport = (zcm_trans_t*)(intptr_t)transportPtr; + int ret = zcm_try_create_from_trans(&I->zcm, transport); + + setNativePtr(env, self, I); + + return ret == ZCM_EOK && I->zcm ? JNI_TRUE : JNI_FALSE; +} + +// RRR (Bendes): Destroy needs to delete I, doesn't it? PASS_THROUGH_FUNC(destroy, destroy, void, ()V) PASS_THROUGH_FUNC(start, start, void, ()V) PASS_THROUGH_FUNC(stop, stop, void, ()V) diff --git a/zcm/java/jni/zcm_zcm_ZCMJNI.h b/zcm/java/jni/zcm_zcm_ZCMJNI.h index fee90943e..f5fbe791b 100644 --- a/zcm/java/jni/zcm_zcm_ZCMJNI.h +++ b/zcm/java/jni/zcm_zcm_ZCMJNI.h @@ -15,6 +15,14 @@ extern "C" { JNIEXPORT jboolean JNICALL Java_zcm_zcm_ZCMJNI_initializeNative (JNIEnv *, jobject, jstring); +/* + * Class: zcm_zcm_ZCMJNI + * Method: initializeNativeFromTransport + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_zcm_zcm_ZCMJNI_initializeNativeFromTransport + (JNIEnv *, jobject, jlong); + /* * Class: zcm_zcm_ZCMJNI * Method: destroy diff --git a/tools/java/zcm/util/BufferedRandomAccessFile.java b/zcm/java/zcm/zcm/BufferedRandomAccessFile.java similarity index 98% rename from tools/java/zcm/util/BufferedRandomAccessFile.java rename to zcm/java/zcm/zcm/BufferedRandomAccessFile.java index 470e99a13..9716ca81f 100644 --- a/tools/java/zcm/util/BufferedRandomAccessFile.java +++ b/zcm/java/zcm/zcm/BufferedRandomAccessFile.java @@ -1,4 +1,4 @@ -package zcm.util; +package zcm.zcm; import java.io.*; @@ -72,21 +72,22 @@ public void seek(long pos) throws IOException bufferSeek(pos); } - public void flush() throws IOException + public boolean flush() throws IOException { - flushBuffer(); + return flushBuffer(); } /** Writes the buffer if it contains any dirty data **/ - void flushBuffer() throws IOException + boolean flushBuffer() throws IOException { if (!bufferDirty) - return; + return false; raf.seek(bufferOffset); raf.write(buffer, 0, bufferLength); bufferDirty = false; + return true; } /** Performs a seek and fills the buffer accordingly. **/ diff --git a/tools/java/zcm/logging/Log.java b/zcm/java/zcm/zcm/Log.java similarity index 97% rename from tools/java/zcm/logging/Log.java rename to zcm/java/zcm/zcm/Log.java index dda9b3908..bc951e4f3 100644 --- a/tools/java/zcm/logging/Log.java +++ b/zcm/java/zcm/zcm/Log.java @@ -1,11 +1,10 @@ -package zcm.logging; +package zcm.zcm; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; -import zcm.util.*; import zcm.zcm.*; /** @@ -73,9 +72,9 @@ public String getPath() /** * Flush any unwritten data to the underlying file descriptor. **/ - public void flush() throws IOException + public boolean flush() throws IOException { - raf.flush(); + return raf.flush(); } /** diff --git a/zcm/java/zcm/zcm/SerialIO.java b/zcm/java/zcm/zcm/SerialIO.java new file mode 100644 index 000000000..488995d93 --- /dev/null +++ b/zcm/java/zcm/zcm/SerialIO.java @@ -0,0 +1,33 @@ +package zcm.zcm; + +import java.nio.ByteBuffer; + +/** + * Interface for serial I/O operations used by the generic serial transport. + * Implementations of this interface handle the actual reading from and writing to + * the underlying serial hardware or communication channel. + */ +public interface SerialIO { + /** + * Read data from the serial interface. + * This method should read up to maxLen bytes from the serial interface + * and store them in the provided ByteBuffer. The buffer is positioned at 0 + * and has a limit of maxLen. + * + * @param buffer ByteBuffer to store the read data (direct buffer, zero-copy) + * @param maxLen maximum number of bytes to read + * @return number of bytes actually read, or 0 if no data available + */ + int get(ByteBuffer buffer, int maxLen); + + /** + * Write data to the serial interface. + * This method should write len bytes from the ByteBuffer to the serial interface. + * The buffer is positioned at 0 and contains len bytes to write. + * + * @param buffer ByteBuffer containing data to write (direct buffer, zero-copy) + * @param len number of bytes to write from the buffer + * @return number of bytes actually written + */ + int put(ByteBuffer buffer, int len); +} diff --git a/zcm/java/zcm/zcm/ZCM.java b/zcm/java/zcm/zcm/ZCM.java index 98d1511bf..63df7d103 100644 --- a/zcm/java/zcm/zcm/ZCM.java +++ b/zcm/java/zcm/zcm/ZCM.java @@ -6,7 +6,7 @@ import java.nio.*; /** Zero Communications and Marshalling Java implementation **/ -public class ZCM +public class ZCM implements AutoCloseable { public class Subscription { @@ -24,12 +24,23 @@ public class Subscription /** Create a new ZCM object, connecting to one or more URLs. If * no URL is specified, ZCM_DEFAULT_URL is used. **/ - public ZCM() throws IOException { this(null); } + public ZCM() throws IOException { this((String)null); } public ZCM(String url) throws IOException { zcmjni = new ZCMJNI(url); } + /** Create a new ZCM object using the provided transport. + * The transport must be valid and properly initialized. + **/ + public ZCM(ZCMTransport transport) throws IOException + { + if (transport == null) { + throw new IllegalArgumentException("Transport cannot be null"); + } + zcmjni = new ZCMJNI(transport); + } + public void start() { zcmjni.start(); } public void stop() { zcmjni.stop(); } @@ -138,7 +149,7 @@ public void close() this.closed = true; } - public void finalize() { if (!this.closed) close(); } + //////////////////////////////////////////////////////////////// diff --git a/zcm/java/zcm/zcm/ZCMGenericSerialTransport.java b/zcm/java/zcm/zcm/ZCMGenericSerialTransport.java new file mode 100644 index 000000000..9c02e4ba8 --- /dev/null +++ b/zcm/java/zcm/zcm/ZCMGenericSerialTransport.java @@ -0,0 +1,125 @@ +package zcm.zcm; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Java wrapper for the ZCM generic serial transport. + * This class provides a Java interface to the C generic serial transport implementation, + * allowing Java applications to communicate over serial interfaces using ZCM. + */ +public class ZCMGenericSerialTransport implements ZCMTransport, AutoCloseable { + + private long nativePtr = 0; + private SerialIO serialIO; + + /** + * Create a new generic serial transport. + * + * @param serialIO the SerialIO implementation for actual hardware communication + * @param mtu Maximum Transmission Unit - maximum message size in bytes + * @param bufSize buffer size for internal circular buffers + * @throws IOException if the transport cannot be created + */ + public ZCMGenericSerialTransport(SerialIO serialIO, int mtu, int bufSize) throws IOException { + if (serialIO == null) { + throw new IllegalArgumentException("SerialIO cannot be null"); + } + if (mtu <= 0) { + throw new IllegalArgumentException("MTU must be positive"); + } + if (bufSize <= 0) { + throw new IllegalArgumentException("Buffer size must be positive"); + } + + this.serialIO = serialIO; + if (!initializeNative(mtu, bufSize)) { + throw new IOException("Failed to create ZCM Generic Serial Transport"); + } + } + + /** + * Initialize the native transport. + * + * @param mtu Maximum Transmission Unit + * @param bufSize buffer size for internal buffers + * @return true if successful, false otherwise + */ + private native boolean initializeNative(int mtu, int bufSize); + + /** + * Get the transport pointer for use with ZCM. + * This method is called by ZCM to get the underlying transport. + * + * @return native transport pointer + */ + private native long getTransportPtr(); + + /** + * Get the native transport pointer for use with ZCM. + * This implements the ZCMTransport interface. + * + * @return native transport pointer + */ + @Override + public long getNativeTransport() { + return getTransportPtr(); + } + + /** + * Destroy the transport and free all resources. + * This implements the ZCMTransport interface. + */ + // RRR (Bendes): I don't think this model works because I think zcm is + // supposed to own the close behavior after zcm is constructed with the + // transport. I'm not positive though + @Override + public native void destroy(); + + /** + * Close the transport and free all resources. + * This implements the AutoCloseable interface for try-with-resources support. + */ + @Override + public void close() { + destroy(); + } + + /** + * Called by native code to read data from the serial interface. + * This method is called from the C layer when data needs to be read. + * + * @param buffer direct ByteBuffer wrapping the native data array + * @param maxLen maximum number of bytes to read + * @return number of bytes actually read + */ + private int nativeGet(ByteBuffer buffer, int maxLen) { + try { + return serialIO.get(buffer, maxLen); + } catch (Exception e) { + // Don't let exceptions propagate through JNI + System.err.println("Exception in SerialIO.get(): " + e.getMessage()); + return 0; + } + } + + /** + * Called by native code to write data to the serial interface. + * This method is called from the C layer when data needs to be written. + * + * @param buffer direct ByteBuffer wrapping the native data array + * @param len number of bytes to write + * @return number of bytes actually written + */ + private int nativePut(ByteBuffer buffer, int len) { + try { + return serialIO.put(buffer, len); + } catch (Exception e) { + // Don't let exceptions propagate through JNI + System.err.println("Exception in SerialIO.put(): " + e.getMessage()); + return 0; + } + } + + +} diff --git a/zcm/java/zcm/zcm/ZCMJNI.java b/zcm/java/zcm/zcm/ZCMJNI.java index c3eab4d27..6b55ac5d4 100644 --- a/zcm/java/zcm/zcm/ZCMJNI.java +++ b/zcm/java/zcm/zcm/ZCMJNI.java @@ -9,6 +9,7 @@ class ZCMJNI private long nativePtr = 0; private native boolean initializeNative(String url); + private native boolean initializeNativeFromTransport(long transportPtr); public ZCMJNI(String url) throws IOException { @@ -20,6 +21,17 @@ public ZCMJNI(String url) throws IOException } } + public ZCMJNI(ZCMTransport transport) throws IOException + { + if (transport == null) { + throw new IllegalArgumentException("Transport cannot be null"); + } + if (!initializeNativeFromTransport(transport.getNativeTransport())) { + throw new IOException("Failed to create ZCM from transport"); + } + } + + // RRR (Bendes): Do we need to delete/destroy zcmjni here too? public native void destroy(); public native void start(); diff --git a/zcm/java/zcm/zcm/ZCMTransport.java b/zcm/java/zcm/zcm/ZCMTransport.java new file mode 100644 index 000000000..960bf7a7d --- /dev/null +++ b/zcm/java/zcm/zcm/ZCMTransport.java @@ -0,0 +1,23 @@ +package zcm.zcm; + +/** + * Interface that all Java ZCM transport implementations must implement. + * This interface provides a bridge between Java transport implementations + * and the native ZCM library. + */ +public interface ZCMTransport { + /** + * Get the native transport pointer for use with the ZCM native library. + * This method returns a pointer to the underlying C transport structure + * that can be passed to zcm_init_from_trans(). + * + * @return native transport pointer as a long value, or 0 if invalid + */ + long getNativeTransport(); + + /** + * Clean up and destroy the transport, freeing all resources. + * After calling this method, the transport should not be used. + */ + void destroy(); +} From cc149d4f26da911530c3743379e47a0bd7ac22b3 Mon Sep 17 00:00:00 2001 From: Jonathan Bendes Date: Thu, 4 Sep 2025 13:01:48 -0400 Subject: [PATCH 2/2] Removed remaining old examples --- .../plugins/ExampleCsvReaderPlugin.java | 80 ------------------- .../plugins/ExampleTranscoderPlugin.java | 61 -------------- 2 files changed, 141 deletions(-) delete mode 100644 examples/java/example/plugins/ExampleCsvReaderPlugin.java delete mode 100644 examples/java/example/plugins/ExampleTranscoderPlugin.java diff --git a/examples/java/example/plugins/ExampleCsvReaderPlugin.java b/examples/java/example/plugins/ExampleCsvReaderPlugin.java deleted file mode 100644 index cf2160e11..000000000 --- a/examples/java/example/plugins/ExampleCsvReaderPlugin.java +++ /dev/null @@ -1,80 +0,0 @@ -package example.plugins; - -import java.io.IOException; -import java.util.ArrayList; - -import zcm.logging.CsvReaderPlugin; -import zcm.logging.Log; -import zcm.zcm.ZCMDataOutputStream; - -import javazcm.types.example_t; - -public class ExampleCsvReaderPlugin extends CsvReaderPlugin -{ - private long eventNo = 0; - - private enum Fields { - LOG_UTIME, - TIMESTAMP, - POS_X, - POS_Y, - POS_Z, - QUAT_W, - QUAT_X, - QUAT_Y, - QUAT_Z, - NUM_RANGES, - RANGES, - NAME, - ENABLED, - NUM_FIELDS - } - - public ArrayList readZcmType(String line) - { - String arr[] = line.split(","); - - example_t e = new example_t(); - - long logEventUtime = Long.parseLong(arr[Fields.LOG_UTIME.ordinal()]); - - e.timestamp = Long.parseLong(arr[Fields.TIMESTAMP.ordinal()]); - - e.position[0] = Double.parseDouble(arr[Fields.POS_X.ordinal()]); - e.position[1] = Double.parseDouble(arr[Fields.POS_Y.ordinal()]); - e.position[2] = Double.parseDouble(arr[Fields.POS_Z.ordinal()]); - - e.orientation[0] = Double.parseDouble(arr[Fields.QUAT_W.ordinal()]); - e.orientation[1] = Double.parseDouble(arr[Fields.QUAT_X.ordinal()]); - e.orientation[2] = Double.parseDouble(arr[Fields.QUAT_Y.ordinal()]); - e.orientation[3] = Double.parseDouble(arr[Fields.QUAT_Z.ordinal()]); - - e.num_ranges = Integer.parseInt(arr[Fields.NUM_RANGES.ordinal()]); - e.ranges = new short[e.num_ranges]; - for (int i = 0; i < e.num_ranges; ++i) - e.ranges[i] = Short.parseShort(arr[Fields.RANGES.ordinal() + i]); - - e.name = arr[Fields.NAME.ordinal() + e.num_ranges - 1]; - e.enabled = Boolean.parseBoolean(arr[Fields.ENABLED.ordinal() + e.num_ranges - 1]); - - ZCMDataOutputStream encodeBuffer = new ZCMDataOutputStream(); - try { - e.encode(encodeBuffer); - } catch (IOException ex) { - System.err.println("Error encoding example2_t"); - ex.printStackTrace(); - return null; - } - - // construct output - Log.Event le = new Log.Event(); - le.data = encodeBuffer.toByteArray(); - le.utime = logEventUtime; - le.eventNumber = eventNo++; - le.channel = "EXAMPLE"; - - ArrayList events = new ArrayList(); - events.add(le); - return events; - } -} diff --git a/examples/java/example/plugins/ExampleTranscoderPlugin.java b/examples/java/example/plugins/ExampleTranscoderPlugin.java deleted file mode 100644 index c7ca3cb10..000000000 --- a/examples/java/example/plugins/ExampleTranscoderPlugin.java +++ /dev/null @@ -1,61 +0,0 @@ -package example.plugins; - -import java.io.IOException; -import java.util.ArrayList; - -import zcm.logging.TranscoderPlugin; -import zcm.logging.Log; -import zcm.zcm.ZCMDataOutputStream; - -import javazcm.types.example_t; -import javazcm.types.example2_t; - -public class ExampleTranscoderPlugin extends TranscoderPlugin -{ - private long eventNo = 0; - - public Long[] handleFingerprints() - { - return new Long[]{ example_t.ZCM_FINGERPRINT, - example2_t.ZCM_FINGERPRINT }; - } - - public ArrayList transcodeMessage(String channel, Object o, long utime, Log.Event ev) - { - if (o instanceof example_t) { - ZCMDataOutputStream encodeBuffer = new ZCMDataOutputStream(); - - example_t e = (example_t) o; - example2_t e2 = new example2_t(); - - e2.timestamp2 = e.timestamp; - e2.position2 = e.position.clone(); - e2.orientation2 = e.orientation.clone(); - e2.num_ranges2 = e.num_ranges; - e2.ranges2 = e.ranges.clone(); - e2.name2 = e.name; - e2.enabled2 = e.enabled; - - try { - e2.encode(encodeBuffer); - } catch (IOException ex) { - System.err.println("Error encoding example2_t"); - ex.printStackTrace(); - return null; - } - - // construct output - Log.Event le = new Log.Event(); - le.data = encodeBuffer.toByteArray(); - le.utime = utime; - le.eventNumber = eventNo++; - le.channel = channel; - - ArrayList events = new ArrayList(); - events.add(le); - return events; - } - - return null; - } -}