Java tutorial
package jmri.jmrit.vsdecoder; /** * Virtual Sound Decoder * * Implements a software "decoder" that responds to throttle inputs and * generates sounds in responds to them. * * Each VSDecoder implements exactly one Sound Profile (describes a particular * type of locomtive, say, an EMD GP7). * */ /* * <hr> * This file is part of JMRI. * <P> * JMRI is free software; you can redistribute it and/or modify it under * the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. See the "COPYING" file for a copy * of this license. * <P> * JMRI is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * <P> * * @author Mark Underwood Copyright (C) 2011 * @version $Revision$ */ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import jmri.Audio; import jmri.DccLocoAddress; import jmri.InstanceManager; import jmri.LocoAddress; import jmri.jmrit.operations.locations.Location; import jmri.jmrit.operations.routes.RouteLocation; import jmri.jmrit.operations.trains.Train; import jmri.jmrit.operations.trains.TrainManager; import jmri.jmrit.vsdecoder.swing.VSDControl; import jmri.jmrit.vsdecoder.swing.VSDManagerFrame; import jmri.util.PhysicalLocation; import org.jdom2.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class VSDecoder implements PropertyChangeListener { boolean initialized = false; // This decoder has been initialized boolean enabled = false; // This decoder is enabled private boolean is_default = false; // This decoder is the default for its file private VSDConfig config; private float tunnelVolume = 0.5f; // List of registered event listeners protected javax.swing.event.EventListenerList listenerList = new javax.swing.event.EventListenerList(); HashMap<String, VSDSound> sound_list; // list of sounds HashMap<String, Trigger> trigger_list; // list of triggers HashMap<String, SoundEvent> event_list; // list of events /** * public VSDecoder(String id, String name) * * Construct a VSDecoder with a given name and ID (system name) * * Parameters: * * @param id (String) System Name of this VSDecoder * @param name (String) Sound Profile name for this VSDecoder */ @Deprecated public VSDecoder(String id, String name) { config = new VSDConfig(); config.setProfileName(name); config.setID(id); sound_list = new HashMap<String, VSDSound>(); trigger_list = new HashMap<String, Trigger>(); event_list = new HashMap<String, SoundEvent>(); // Force re-initialization initialized = _init(); } /** * public VSDecoder(VSDConfig cfg) * * Construct a VSDecoder with the given system name (id) and configuration * (config) * * Parameters: * * @param cfg (VSDConfig) Configuration */ public VSDecoder(VSDConfig cfg) { config = cfg; sound_list = new HashMap<String, VSDSound>(); trigger_list = new HashMap<String, Trigger>(); event_list = new HashMap<String, SoundEvent>(); // Force re-initialization initialized = _init(); try { VSDFile vsdfile = new VSDFile(config.getVSDPath()); if (vsdfile.isInitialized()) { log.debug("Constructor: vsdfile init OK, loading XML..."); this.setXml(vsdfile, config.getProfileName()); } else { log.debug("Constructor: vsdfile init FAILED."); initialized = false; } } catch (java.util.zip.ZipException e) { log.error("ZipException loading VSDecoder from " + config.getVSDPath()); // would be nice to pop up a dialog here... } catch (java.io.IOException ioe) { log.error("IOException loading VSDecoder from " + config.getVSDPath()); // would be nice to pop up a dialog here... } // Since the Config already has the address set, we need to call // our own setAddress() to register the throttle listener this.setAddress(config.getLocoAddress()); this.enable(); if (log.isDebugEnabled()) { log.debug("VSDecoder Init Complete. Audio Objects Created:"); for (String s : InstanceManager.audioManagerInstance().getSystemNameList(Audio.SOURCE)) { log.debug("\tSource: " + s); } for (String s : InstanceManager.audioManagerInstance().getSystemNameList(Audio.BUFFER)) { log.debug("\tBuffer: " + s); } } } /** * public VSDecoder(String id, String name, String path) * * Construct a VSDecoder with the given system name (id), profile name and * VSD file path * * Parameters: * * @param id (String) System name for this VSDecoder * @param name (String) Profile name * @param path (String) Path to a VSD file to pull the given Profile from */ public VSDecoder(String id, String name, String path) { config = new VSDConfig(); config.setProfileName(name); config.setID(id); sound_list = new HashMap<String, VSDSound>(); trigger_list = new HashMap<String, Trigger>(); event_list = new HashMap<String, SoundEvent>(); // Force re-initialization initialized = _init(); config.setVSDPath(path); try { VSDFile vsdfile = new VSDFile(path); if (vsdfile.isInitialized()) { log.debug("Constructor: vsdfile init OK, loading XML..."); this.setXml(vsdfile, name); } else { log.debug("Constructor: vsdfile init FAILED."); initialized = false; } } catch (java.util.zip.ZipException e) { log.error("ZipException loading VSDecoder from " + path); // would be nice to pop up a dialog here... } catch (java.io.IOException ioe) { log.error("IOException loading VSDecoder from " + path); // would be nice to pop up a dialog here... } } private boolean _init() { // Do nothing for now this.enable(); return (true); } /** * public String getID() * * Get the ID (System Name) of this VSDecoder * * @return (String) system name of this VSDecoder */ public String getID() { return (config.getID()); } /** * public boolean isInitialized() * * Check whether this VSDecoder has completed initialization * * @return (boolean) true if initialization is complete. */ public boolean isInitialized() { return (initialized); } /** * public void setVSDFilePath(String p) * * Set the VSD File path for this VSDecoder to use * * @param p (String) path to VSD File */ public void setVSDFilePath(String p) { config.setVSDPath(p); } /** * public String getVSDFilePath() * * Get the current VSD File path for this VSDecoder * * @return (String) path to VSD file */ public String getVSDFilePath() { return (config.getVSDPath()); } // VSDecoder Events /** * public String addEventListener(VSDecoderListener listener) * * Add a listener for this object's events * * @param listener handle */ public void addEventListener(VSDecoderListener listener) { listenerList.add(VSDecoderListener.class, listener); } /** * public String removeEventListener(VSDecoderListener listener) * * Remove a listener for this object's events * * @param listener handle */ public void removeEventListener(VSDecoderListener listener) { listenerList.remove(VSDecoderListener.class, listener); } /** * Fire an event to this object's listeners */ private void fireMyEvent(VSDecoderEvent evt) { for (VSDecoderListener l : listenerList.getListeners(VSDecoderListener.class)) { l.eventAction(evt); } } /** * public void windowChange(java.awt.event.WindowEvent e) * * Handle Window events from this VSDecoder's GUI window. * * @param e the window event to handle */ public void windowChange(java.awt.event.WindowEvent e) { log.debug("decoder.windowChange() - " + e.toString()); log.debug("param string = " + e.paramString()); // if (e.paramString().equals("WINDOW_CLOSING")) { // Shut down the sounds. this.shutdown(); // } } /** * public void shutdown() * * Shut down this VSDecoder and all of its associated sounds. * */ public void shutdown() { log.debug("Shutting down sounds..."); for (VSDSound vs : sound_list.values()) { log.debug("Stopping sound: " + vs.getName()); vs.shutdown(); } } /** * protected void throttlePropertyChange(PropertyChangeEvent event) * * Handle the details of responding to a PropertyChangeEvent from a * throttle. * * @param event (PropertyChangeEvent) Throttle event to respond to */ protected void throttlePropertyChange(PropertyChangeEvent event) { // WARNING: FRAGILE CODE // This will break if the return type of the event.getOld/NewValue() changes. String eventName = event.getPropertyName(); Object oldValue = event.getOldValue(); Object newValue = event.getNewValue(); // Skip this if disabled if (!enabled) { log.debug("VSDecoder disabled. Take no action."); return; } log.warn("VSDecoderPane throttle property change: " + eventName); if (oldValue != null) { log.warn("Old: " + oldValue.toString()); } if (newValue != null) { log.warn("New: " + newValue.toString()); } // Iterate through the list of sound events, forwarding the propertyChange event. for (SoundEvent t : event_list.values()) { t.propertyChange(event); } // Iterate through the list of triggers, forwarding the propertyChange event. for (Trigger t : trigger_list.values()) { t.propertyChange(event); } } // DCC-specific and unused. Deprecate this. @Deprecated public void releaseAddress(int number, boolean isLong) { // remove the listener, if we can... } // DCC-specific. Deprecate this. @Deprecated public void setAddress(int number, boolean isLong) { this.setAddress(new DccLocoAddress(number, isLong)); } /** * public void setAddress(LocoAddress l) * * Set this VSDecoder's LocoAddress, and register to follow events from the * throttle with this address. * * @param l (LocoAddress) LocoAddress to be followed */ public void setAddress(LocoAddress l) { // Hack for ThrottleManager Dcc dependency config.setLocoAddress(l); // DccLocoAddress dl = new DccLocoAddress(l.getNumber(), l.getProtocol()); jmri.InstanceManager.throttleManagerInstance().attachListener(config.getDccAddress(), new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent event) { log.debug("property change name " + event.getPropertyName() + " old " + event.getOldValue() + " new " + event.getNewValue()); throttlePropertyChange(event); } }); log.debug("VSDecoder: Address set to " + config.getLocoAddress().toString()); } /** * public LocoAddress getAddress() * * Get the currently assigned LocoAddress * * @return the currently assigned LocoAddress */ public LocoAddress getAddress() { return (config.getLocoAddress()); } /** * public float getMasterVolume() * * Get the current master volume setting for this VSDecoder * * @return (float) volume level (0.0 - 1.0) */ public float getMasterVolume() { return (config.getVolume()); } /** * public void setMasterVolume(float vol) * * Set the current master volume setting for this VSDecoder * * @param vol (float) volume level (0.0 - 1.0) */ public void setMasterVolume(float vol) { log.debug("VSD: float volume = " + vol); config.setVolume(vol); for (VSDSound vs : sound_list.values()) { vs.setVolume(vol); } } /** * public boolean isMuted() * * Is this VSDecoder muted? * * @return true if muted. */ public boolean isMuted() { return (false); } /** * public void mute(boolean m) * * Mute or un-mute this VSDecoder * * @param m (boolean) true to mute, false to un-mute */ public void mute(boolean m) { for (VSDSound vs : sound_list.values()) { vs.mute(m); } } /** * public void setPosition(PhysicalLocation p) * * set the x/y/z position in the soundspace of this VSDecoder Translates the * given position to a position relative to the listener for the component * VSDSounds. * * The idea is that the user-preference Listener Position (relative to the * USER's chosen origin) is always the OpenAL Context's origin. * * @param p (PhysicalLocation) location relative to the user's chosen * Origin. */ public void setPosition(PhysicalLocation p) { // Store the actual position relative to the user's Origin locally. config.setPhysicalLocation(p); log.debug("( " + this.getAddress() + ") Set Position: " + p.toString()); // Give all of the VSDSound objects the position translated relative to the listener position. // This is a workaround for OpenAL requiring the listener position to always be at (0,0,0). /* * PhysicalLocation ref = VSDecoderManager.instance().getVSDecoderPreferences().getListenerPhysicalLocation(); * if (ref == null) ref = PhysicalLocation.Origin; */ for (VSDSound s : sound_list.values()) { // s.setPosition(PhysicalLocation.translate(p, ref)); s.setPosition(p); } // Set (relative) volume for this location (in case we're in a tunnel) float tv = config.getVolume(); if (p.isTunnel()) { tv *= tunnelVolume; log.debug("VSD: Tunnel volume: " + tv); } else { log.debug("VSD: Not in tunnel. Volume = " + tv); } for (VSDSound vs : sound_list.values()) { vs.setVolume(tv); } fireMyEvent(new VSDecoderEvent(this, VSDecoderEvent.EventType.LOCATION_CHANGE, p)); } /** * public PhysicalLocation getPosition() * * Get the current x/y/z position in the soundspace of this VSDecoder * * @return PhysicalLocation location of this VSDecoder */ public PhysicalLocation getPosition() { return (config.getPhysicalLocation()); } /** * public void propertyChange(PropertyChangeEvent evt) * * Respond to property change events from this VSDecoder's GUI * * @param evt (PropertyChangeEvent) event to respond to */ @SuppressWarnings("cast") public void propertyChange(PropertyChangeEvent evt) { String property = evt.getPropertyName(); // Respond to events from the new GUI. if (evt.getSource() instanceof VSDControl) { if (property.equals(VSDControl.PCIDMap.get(VSDControl.PropertyChangeID.OPTION_CHANGE))) { Train selected_train = TrainManager.instance().getTrainByName((String) evt.getNewValue()); if (selected_train != null) { selected_train.addPropertyChangeListener(this); } } return; } // Respond to events from the old GUI. if ((property.equals(VSDManagerFrame.PCIDMap.get(VSDManagerFrame.PropertyChangeID.MUTE))) || (property.equals(VSDecoderPane.PCIDMap.get(VSDecoderPane.PropertyChangeID.MUTE)))) { // Either GUI Mute button log.debug("VSD: Mute change. value = " + evt.getNewValue()); Boolean b = (Boolean) evt.getNewValue(); this.mute(b.booleanValue()); } else if ((property.equals(VSDManagerFrame.PCIDMap.get(VSDManagerFrame.PropertyChangeID.VOLUME_CHANGE))) || (property.equals(VSDecoderPane.PCIDMap.get(VSDecoderPane.PropertyChangeID.VOLUME_CHANGE)))) { // Either GUI Volume slider log.debug("VSD: Volume change. value = " + evt.getNewValue()); // Slider gives integer 0-100. Need to change that to a float 0.0-1.0 this.setMasterVolume((1.0f * (Integer) evt.getNewValue()) / 100.0f); } else if (property.equals(VSDecoderPane.PCIDMap.get(VSDecoderPane.PropertyChangeID.ADDRESS_CHANGE))) { // OLD GUI Address Change log.debug("Decoder set address = " + (LocoAddress) evt.getNewValue()); this.setAddress((LocoAddress) evt.getNewValue()); this.enable(); } else if (property.equals(Train.TRAIN_LOCATION_CHANGED_PROPERTY)) { // Train Location Move (either GUI) PhysicalLocation p = getTrainPosition((Train) evt.getSource()); if (p != null) { this.setPosition(getTrainPosition((Train) evt.getSource())); } else { log.debug("Train has null position"); this.setPosition(new PhysicalLocation()); } } else if (property.equals(Train.STATUS_CHANGED_PROPERTY)) { // Train Status change (either GUI) String status = (String) evt.getNewValue(); log.debug("Train status changed: " + status); log.debug("New Location: " + getTrainPosition((Train) evt.getSource())); if ((status.startsWith(Train.BUILT)) || (status.startsWith(Train.PARTIAL_BUILT))) { log.debug("Train built. status = " + status); PhysicalLocation p = getTrainPosition((Train) evt.getSource()); if (p != null) { this.setPosition(getTrainPosition((Train) evt.getSource())); } else { log.debug("Train has null position"); this.setPosition(new PhysicalLocation()); } } } } // Methods for handling location tracking based on JMRI Operations /** * protected PhysicalLocation getTrainPosition(Train t) * * Get the physical location of the given Operations Train * * @param t (Train) the Train to interrogate * @return PhysicalLocation location of the train */ protected PhysicalLocation getTrainPosition(Train t) { if (t == null) { log.debug("Train is null."); return (null); } RouteLocation rloc = t.getCurrentLocation(); if (rloc == null) { log.debug("RouteLocation is null."); return (null); } Location loc = rloc.getLocation(); if (loc == null) { log.debug("Location is null."); return (null); } return (loc.getPhysicalLocation()); } // Methods for handling the underlying sounds /** * public VSDSound getSound(String name) * * Retrieve the VSDSound with the given system name * * @param name (String) System name of the requested VSDSound * @return VSDSound the requested sound */ public VSDSound getSound(String name) { return (sound_list.get(name)); } /** * public void toggleBell() * * Turn the bell sound on/off * */ public void toggleBell() { VSDSound snd = sound_list.get("BELL"); if (snd.isPlaying()) { snd.stop(); } else { snd.loop(); } } /** * public void toggleHorn() * * Turn the horn sound on/off * */ public void toggleHorn() { VSDSound snd = sound_list.get("HORN"); if (snd.isPlaying()) { snd.stop(); } else { snd.loop(); } } /** * public void playHorn() * * Turn the horn sound on * */ public void playHorn() { VSDSound snd = sound_list.get("HORN"); snd.loop(); } /** * public void shortHorn() * * Turn the horn sound on (Short burst) * */ public void shortHorn() { VSDSound snd = sound_list.get("HORN"); snd.play(); } /** * public void stopHorn() * * Turn the horn sound off * */ public void stopHorn() { VSDSound snd = sound_list.get("HORN"); snd.stop(); } // Java Bean set/get Functions /** * public void setProfileName(String pn) * * Set the profile name to the given string * * @param pn (String) : name of the profile to set */ public void setProfileName(String pn) { config.setProfileName(pn); } /** * public String getProfileName() * * get the currently selected profile name * * @return (String) name of the currently selected profile */ public String getProfileName() { return (config.getProfileName()); } /** * public void enable() * * Enable this VSDecoder * */ public void enable() { enabled = true; } /** * public void disable() * * Disable this VSDecoder * */ public void disable() { enabled = false; } /** * public Collection<SoundEvent> getEventList() * * Get a Collection of SoundEvents associated with this VSDecoder * * @return Collection<SoundEvent> collection of SoundEvents */ public Collection<SoundEvent> getEventList() { return (event_list.values()); } /** * public boolean isDefault() * * True if this is the default VSDecoder * * @return boolean true if this is the default VSDecoder */ public boolean isDefault() { return (is_default); } /** * public void isDefault(boolean d) * * Set whether this is the default VSDecoder or not * * @param d (boolean) True to set this as the default, False if not. */ public void setDefault(boolean d) { is_default = d; } /** * public Element getXML() * * Get an XML representation of this VSDecoder Includes a subtree of * Elements for all of the associated SoundEvents, Triggers, VSDSounds, etc. * * @return Element XML Element for this VSDecoder */ public Element getXml() { Element me = new Element("vsdecoder"); ArrayList<Element> le = new ArrayList<Element>(); me.setAttribute("name", this.config.getProfileName()); // If this decoder is marked as default, add the default Element. if (is_default) { me.addContent(new Element("default")); } for (SoundEvent se : event_list.values()) { le.add(se.getXml()); } for (VSDSound vs : sound_list.values()) { le.add(vs.getXml()); } for (Trigger t : trigger_list.values()) { le.add(t.getXml()); } me.addContent(le); // Need to add whatever else here. return (me); } /* * @Deprecated public void setXml(Element e) { this.setXml(e, null); } * * @Deprecated public void setXml(Element e, VSDFile vf) { this.setXml(vf); } * * @Deprecated public void setXml(VSDFile vf) { } */ /** * public void setXML(VSDFile vf, String pn) * * Build this VSDecoder from an XML representation * * @param vf (VSDFile) : VSD File to pull the XML from * @param pn (String) : Parameter Name to find within the VSD File. */ @SuppressWarnings({ "cast" }) public void setXml(VSDFile vf, String pn) { Iterator<Element> itr; Element e = null; Element el = null; SoundEvent se; if (vf == null) { log.debug("Null VSD File Name"); return; } log.debug("VSD File Name = " + vf.getName()); // need to choose one. this.setVSDFilePath(vf.getName()); // Find the <profile/> element that matches the name pn // List<Element> profiles = vf.getRoot().getChildren("profile"); // java.util.Iterator i = profiles.iterator(); java.util.Iterator<Element> i = vf.getRoot().getChildren("profile").iterator(); while (i.hasNext()) { e = i.next(); if (e.getAttributeValue("name").equals(pn)) { break; } } // E is now the first <profile/> in vsdfile that matches pn. if (e == null) { // No matching profile name found. return; } // Set this decoder's name. this.setProfileName(e.getAttributeValue("name")); log.debug("Decoder Name = " + e.getAttributeValue("name")); // Read and create all of its components. // Check for default element. if (e.getChild("default") != null) { log.debug("" + getProfileName() + "is default."); is_default = true; } else { is_default = false; } // +++ DEBUG // Log and print all of the child elements. itr = (e.getChildren()).iterator(); while (itr.hasNext()) { // Pull each element from the XML file. el = itr.next(); log.debug("Element: " + el.toString()); if (el.getAttribute("name") != null) { log.debug(" Name: " + el.getAttributeValue("name")); log.debug(" type: " + el.getAttributeValue("type")); } } // --- DEBUG // First, the sounds. String prefix = "" + this.getID() + ":"; log.debug("VSDecoder " + this.getID() + " prefix = " + prefix); itr = (e.getChildren("sound")).iterator(); while (itr.hasNext()) { el = (Element) itr.next(); if (el.getAttributeValue("type") == null) { // Empty sound. Skip. log.debug("Skipping empty Sound."); continue; } else if (el.getAttributeValue("type").equals("configurable")) { // Handle configurable sounds. ConfigurableSound cs = new ConfigurableSound(prefix + el.getAttributeValue("name")); cs.setXml(el, vf); sound_list.put(el.getAttributeValue("name"), cs); } else if (el.getAttributeValue("type").equals("diesel")) { // Handle a Diesel Engine sound DieselSound es = new DieselSound(prefix + el.getAttributeValue("name")); es.setXml(el, vf); sound_list.put(el.getAttributeValue("name"), es); } else if (el.getAttributeValue("type").equals("diesel3")) { // Handle a Diesel Engine sound Diesel3Sound es = new Diesel3Sound(prefix + el.getAttributeValue("name")); es.setXml(el, vf); sound_list.put(el.getAttributeValue("name"), es); } else if (el.getAttributeValue("type").equals("steam")) { // Handle a Diesel Engine sound SteamSound es = new SteamSound(prefix + el.getAttributeValue("name")); es.setXml(el, vf); sound_list.put(el.getAttributeValue("name"), es); } else { // TODO: Some type other than configurable sound. Handle appropriately } } // Next, grab all of the SoundEvents // Have to do the sounds first because the SoundEvent's setXml() will // expect to be able to look it up. itr = (e.getChildren("sound-event")).iterator(); while (itr.hasNext()) { el = (Element) itr.next(); switch (SoundEvent.ButtonType.valueOf(el.getAttributeValue("buttontype").toUpperCase())) { case MOMENTARY: se = new MomentarySoundEvent(el.getAttributeValue("name")); break; case TOGGLE: se = new ToggleSoundEvent(el.getAttributeValue("name")); break; case ENGINE: se = new EngineSoundEvent(el.getAttributeValue("name")); break; case NONE: default: se = new SoundEvent(el.getAttributeValue("name")); } se.setParent(this); se.setXml(el, vf); event_list.put(se.getName(), se); } // Handle other types of children similarly here. // Check for an existing throttle and update speed if it exists. Float s = (Float) InstanceManager.throttleManagerInstance().getThrottleInfo(config.getDccAddress(), "SpeedSetting"); if (s != null) { // Mimic a throttlePropertyChange to propagate the current (init) speed setting of the throttle. log.debug("Existing Throttle found. Speed = " + s); this.throttlePropertyChange(new PropertyChangeEvent(this, "SpeedSetting", null, s)); } else { log.debug("No existing throttle found."); } } private static final Logger log = LoggerFactory.getLogger(VSDecoder.class.getName()); }