001package jmri.implementation;
002
003import java.net.URL;
004import java.util.*;
005
006import jmri.SignalHead;
007import jmri.SignalSystem;
008import jmri.util.FileUtil;
009import org.jdom2.Element;
010import org.jdom2.JDOMException;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * Default implementation of a basic signal mast aspect - appearance mapping.
016 * <p>
017 * The default contents are taken from the NamedBeanBundle properties file. This
018 * makes creation a little more heavy-weight, but speeds operation.
019 *
020 * @author Bob Jacobsen Copyright (C) 2009
021 */
022public class DefaultSignalAppearanceMap extends AbstractNamedBean implements jmri.SignalAppearanceMap {
023
024    public DefaultSignalAppearanceMap(String systemName, String userName) {
025        super(systemName, userName);
026    }
027
028    public DefaultSignalAppearanceMap(String systemName) {
029        super(systemName);
030    }
031
032    @Override
033    public String getBeanType() {
034        return Bundle.getMessage("BeanNameSignalAppMap");
035    }
036
037    static public DefaultSignalAppearanceMap getMap(String signalSystemName, String aspectMapName) {
038        log.debug("getMap signalSystem= \"{}\", aspectMap= \"{}\"", signalSystemName, aspectMapName);
039        DefaultSignalAppearanceMap map = maps.get("map:" + signalSystemName + ":" + aspectMapName);
040        if (map == null) {
041            log.debug("not located, request loadMap signalSystem= \"{}\", aspectMap= \"{}\"", signalSystemName, aspectMapName);
042            map = loadMap(signalSystemName, aspectMapName);
043        }
044        return map;
045    }
046
047    // added 3.9.7 so CATS can create own implementations
048    protected void registerMap() {
049        maps.put(getSystemName(), this);
050    }
051
052    // added 3.9.7 so CATS can create own implementations
053    static public DefaultSignalAppearanceMap findMap(String systemName) {
054        return maps.get(systemName);
055    }
056
057    static DefaultSignalAppearanceMap loadMap(String signalSystemName, String aspectMapName) {
058        DefaultSignalAppearanceMap map
059                = new DefaultSignalAppearanceMap("map:" + signalSystemName + ":" + aspectMapName);
060        maps.put("map:" + signalSystemName + ":" + aspectMapName, map);
061
062        String path = "signals/" + signalSystemName + "/appearance-" + aspectMapName + ".xml";
063        URL file = FileUtil.findURL(path, "resources", "xml");
064        if (file == null) {
065            log.error("appearance file (xml/{}) doesn't exist", path);
066            throw new IllegalArgumentException("appearance file (xml/" + path + ") doesn't exist");
067        }
068        jmri.jmrit.XmlFile xf = new jmri.jmrit.XmlFile() {
069        };
070        Element root;
071        try {
072            root = xf.rootFromURL(file);
073            // get appearances
074
075            List<Element> l = root.getChild("appearances").getChildren("appearance");
076
077            // find all appearances, include them by aspect name, 
078            log.debug("   reading {} aspectname elements", l.size());
079            for (int i = 0; i < l.size(); i++) {
080                String name = l.get(i).getChild("aspectname").getText();
081                log.debug("aspect name {}", name);
082
083                // add 'show' sub-elements as ints
084                List<Element> c = l.get(i).getChildren("show");
085
086                int[] appearances = new int[c.size()];
087                for (int j = 0; j < c.size(); j++) {
088                    // note: includes setting name; redundant, but needed
089                    int ival;
090                    String sval = c.get(j).getText().toUpperCase();
091                    if (sval.equals("LUNAR")) {
092                        ival = SignalHead.LUNAR;
093                    } else if (sval.equals("GREEN")) {
094                        ival = SignalHead.GREEN;
095                    } else if (sval.equals("YELLOW")) {
096                        ival = SignalHead.YELLOW;
097                    } else if (sval.equals("RED")) {
098                        ival = SignalHead.RED;
099                    } else if (sval.equals("FLASHLUNAR")) {
100                        ival = SignalHead.FLASHLUNAR;
101                    } else if (sval.equals("FLASHGREEN")) {
102                        ival = SignalHead.FLASHGREEN;
103                    } else if (sval.equals("FLASHYELLOW")) {
104                        ival = SignalHead.FLASHYELLOW;
105                    } else if (sval.equals("FLASHRED")) {
106                        ival = SignalHead.FLASHRED;
107                    } else if (sval.equals("DARK")) {
108                        ival = SignalHead.DARK;
109                    } else {
110                        log.error("found invalid content: {}", sval);
111                        throw new JDOMException("invalid content: " + sval);
112                    }
113
114                    appearances[j] = ival;
115                }
116                map.addAspect(name, appearances);
117
118                List<Element> img = l.get(i).getChildren("imagelink");
119                loadImageMaps(img, name, map);
120
121                // now add the rest of the attributes
122                Hashtable<String, String> hm = new Hashtable<String, String>();
123
124                List<Element> a = l.get(i).getChildren();
125
126                for (int j = 0; j < a.size(); j++) {
127                    String key = a.get(j).getName();
128                    String value = a.get(j).getText();
129                    hm.put(key, value);
130                }
131
132                map.aspectAttributeMap.put(name, hm);
133            }
134            loadSpecificMap(signalSystemName, aspectMapName, map, root);
135            loadAspectRelationMap(signalSystemName, aspectMapName, map, root);
136            log.debug("loading complete");
137        } catch (java.io.IOException | org.jdom2.JDOMException e) {
138            log.error("error reading file {}", file.getPath(), e);
139            return null;
140        }
141
142        return map;
143    }
144
145    static void loadImageMaps(List<Element> img, String name, DefaultSignalAppearanceMap map) {
146        Hashtable<String, String> images = new Hashtable<String, String>();
147        for (int j = 0; j < img.size(); j++) {
148            String key = "default";
149            if ((img.get(j).getAttribute("type")) != null) {
150                key = img.get(j).getAttribute("type").getValue();
151            }
152            String value = img.get(j).getText();
153            images.put(key, value);
154        }
155        map.aspectImageMap.put(name, images);
156    }
157
158    static void loadSpecificMap(String signalSystemName, String aspectMapName, DefaultSignalAppearanceMap SMmap, Element root) {
159        log.debug("load specific signalSystem= \"{}\", aspectMap= \"{}\"{}", aspectMapName, signalSystemName);
160        loadSpecificAspect(signalSystemName, aspectMapName, HELD, SMmap, root);
161        loadSpecificAspect(signalSystemName, aspectMapName, DANGER, SMmap, root);
162        loadSpecificAspect(signalSystemName, aspectMapName, PERMISSIVE, SMmap, root);
163        loadSpecificAspect(signalSystemName, aspectMapName, DARK, SMmap, root);
164    }
165
166    static void loadSpecificAspect(String signalSystemName, String aspectMapName, int aspectType, DefaultSignalAppearanceMap SMmap, Element root) {
167
168        String child;
169        switch (aspectType) {
170            case HELD:
171                child = "held";
172                break;
173            case DANGER:
174                child = "danger";
175                break;
176            case PERMISSIVE:
177                child = "permissive";
178                break;
179            case DARK:
180                child = "dark";
181                break;
182            default:
183                child = "danger";
184        }
185
186        String appearance = null;
187        if (root.getChild("specificappearances") == null || root.getChild("specificappearances").getChild(child) == null) {
188            log.debug("appearance not configured {}", child);
189            return;
190        }
191        try {
192            appearance = root.getChild("specificappearances").getChild(child).getChild("aspect").getText();
193            SMmap.specificMaps.put(aspectType, appearance);
194        } catch (java.lang.NullPointerException e) {
195            log.debug("aspect for specific appearance not configured {}", child);
196        }
197
198        try {
199            List<Element> img = root.getChild("specificappearances").getChild(child).getChildren("imagelink");
200            String name = "$" + child;
201            if (img.size() == 0) {
202                if (appearance != null) {
203                    //We do not have any specific images created, therefore we use the
204                    //those associated with the aspect.
205                    List<String> app = SMmap.getImageTypes(appearance);
206                    Hashtable<String, String> images = new Hashtable<String, String>();
207                    String type = "";
208                    for (int i = 0; i < app.size(); i++) {
209                        type = SMmap.getImageLink(appearance, app.get(i));
210                        images.put(app.get(i), type);
211                    }
212                    //We will register the last aspect as a default.
213                    images.put("default", type);
214                    SMmap.aspectImageMap.put(name, images);
215                }
216            } else {
217                loadImageMaps(img, name, SMmap);
218                Hashtable<String, String> hm = new Hashtable<String, String>();
219
220                //Register the last aspect as the default
221                String key = img.get(img.size() - 1).getName();
222                String value = img.get(img.size() - 1).getText();
223                hm.put(key, value);
224
225                SMmap.aspectAttributeMap.put(name, hm);
226            }
227        } catch (java.lang.NullPointerException e) {
228            //Considered Normal if held aspect uses default signal appearance
229        }
230    }
231
232    static void loadAspectRelationMap(String signalSystemName, String aspectMapName, DefaultSignalAppearanceMap SMmap, Element root) {
233        if (log.isDebugEnabled()) {
234            log.debug("load aspect relation map signalSystem= \"{}\", aspectMap= \"{}\"", signalSystemName, aspectMapName);
235        }
236
237        try {
238            List<Element> l = root.getChild("aspectMappings").getChildren("aspectMapping");
239            for (int i = 0; i < l.size(); i++) {
240                String advanced = l.get(i).getChild("advancedAspect").getText();
241
242                List<Element> o = l.get(i).getChildren("ourAspect");
243                String[] appearances = new String[o.size()];
244                for (int j = 0; j < o.size(); j++) {
245                    appearances[j] = o.get(j).getText();
246                }
247                SMmap.aspectRelationshipMap.put(advanced, appearances);
248            }
249
250        } catch (java.lang.NullPointerException e) {
251            log.debug("appearance not configured");
252            return;
253        }
254    }
255
256    /**
257     * Get a property associated with a specific aspect.
258     */
259    @Override
260    public String getProperty(String aspect, String key) {
261        return aspectAttributeMap.get(aspect).get(key);
262    }
263
264    @Override
265    public String getImageLink(String aspect, String type) {
266        if (type == null || type.equals("")) {
267            type = "default";
268        }
269        String value;
270        try {
271            value = aspectImageMap.get(aspect).get(type);
272            //if we don't return a valid image set, then we will use which ever set is loaded in the getProperty
273            if (value == null) {
274                value = getProperty(aspect, "imagelink");
275            }
276        } catch (java.lang.NullPointerException e) {
277            /* Can be considered normal for situations where a specific aspect
278             has been asked for but it hasn't yet been loaded or configured */
279            value = "";
280        }
281        return value;
282    }
283
284    @Override
285    public Vector<String> getImageTypes(String aspect) {
286        if (!checkAspect(aspect)) {
287            return new Vector<String>();
288        }
289        Enumeration<String> e = aspectImageMap.get(aspect).keys();
290        Vector<String> v = new Vector<String>();
291        while (e.hasMoreElements()) {
292            v.add(e.nextElement());
293        }
294        return v;
295    }
296
297    protected Hashtable<String, Hashtable<String, String>> aspectAttributeMap
298            = new Hashtable<String, Hashtable<String, String>>();
299
300    protected Hashtable<String, Hashtable<String, String>> aspectImageMap
301            = new Hashtable<String, Hashtable<String, String>>();
302
303    static HashMap<String, DefaultSignalAppearanceMap> maps
304            = new LinkedHashMap<String, DefaultSignalAppearanceMap>();
305
306    protected Hashtable<Integer, String> specificMaps
307            = new Hashtable<Integer, String>();
308
309    protected Hashtable<String, String[]> aspectRelationshipMap
310            = new Hashtable<String, String[]>();
311
312    public void loadDefaults() {
313
314        log.debug("start loadDefaults");
315
316        String ra;
317        ra = Bundle.getMessage("SignalAspectDefaultRed");
318        if (ra != null) {
319            addAspect(ra, new int[]{SignalHead.RED});
320        } else {
321            log.error("no default red aspect");
322        }
323
324        ra = Bundle.getMessage("SignalAspectDefaultYellow");
325        if (ra != null) {
326            addAspect(ra, new int[]{SignalHead.YELLOW});
327        } else {
328            log.error("no default yellow aspect");
329        }
330
331        ra = Bundle.getMessage("SignalAspectDefaultGreen");
332        if (ra != null) {
333            addAspect(ra, new int[]{SignalHead.GREEN});
334        } else {
335            log.error("no default green aspect");
336        }
337    }
338
339    @Override
340    public boolean checkAspect(String aspect) {
341        if (aspect == null) {
342            return false;
343        }
344        return table.containsKey(aspect);// != null;
345    }
346
347    public void addAspect(String aspect, int[] appearances) {
348        if (log.isDebugEnabled()) {
349            log.debug("add aspect \"{}\" for {} heads {}", aspect, appearances.length, appearances[0]);
350        }
351        table.put(aspect, appearances);
352    }
353
354    /**
355     * Provide the Aspect elements to GUI and store methods.
356     *
357     * @return all aspects in this signal mast appearance map, in the order defined in xml definition
358     */
359    @Override
360    public Enumeration<String> getAspects() {
361        log.debug("list of aspects provided");
362        return new Vector<String>(table.keySet()).elements();  // this will be greatly simplified when we can just return keySet
363    }
364
365    @Override
366    public String getSpecificAppearance(int appearance) {
367        if (specificMaps.containsKey(appearance)) {
368            return specificMaps.get(appearance);
369        }
370        return null;
371    }
372
373    /**
374     * {@inheritDoc}
375     */
376    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
377        justification = "null returned is documented to mean no valid result")
378    @Override
379    public String[] getValidAspectsForAdvancedAspect(String advancedAspect) {
380        if (aspectRelationshipMap == null) {
381            log.error("aspect relationships have not been defined or loaded");
382            throw new IllegalArgumentException("aspect relationships have not been defined or loaded");
383        }
384        if (advancedAspect == null) {
385            String[] danger = new String[1];
386            danger[0] = getSpecificAppearance(DANGER);
387            return danger;
388        }
389        if (aspectRelationshipMap.containsKey(advancedAspect)) {
390            //String[] validAspects = aspectRelationMap.get(advancedAspect);
391            return aspectRelationshipMap.get(advancedAspect);
392        }
393        return null;
394    }
395
396    @Override
397    public SignalSystem getSignalSystem() {
398        return systemDefn;
399    }
400
401    public void setSignalSystem(SignalSystem t) {
402        systemDefn = t;
403    }
404    protected SignalSystem systemDefn;
405
406    /**
407     * {@inheritDoc}
408     *
409     * This method returns a constant result on the DefaultSignalAppearanceMap.
410     *
411     * @return {@link jmri.NamedBean#INCONSISTENT}
412     */
413    @Override
414    public int getState() {
415        return INCONSISTENT;
416    }
417
418    /**
419     * {@inheritDoc}
420     *
421     * This method has no effect on the DefaultSignalAppearanceMap.
422     */
423    @Override
424    public void setState(int s) {
425        // do nothing
426    }
427
428    public int[] getAspectSettings(String aspect) {
429        return table.get(aspect);
430    }
431
432    protected HashMap<String, int[]> table = new LinkedHashMap<String, int[]>();
433
434    @Override
435    /**
436     * {@inheritDoc}
437     */
438    public String summary() {
439        StringBuilder retval = new StringBuilder();
440        retval.append(toString());
441        retval.append("\n  BeanType: "+getBeanType());
442                
443        retval.append("\n  aspects:");
444        Enumeration<String> values = getAspects();
445        while (values.hasMoreElements()) {
446            String aspect = values.nextElement();
447            retval.append("\n    aspect: "+aspect);
448            retval.append("\n       len aspectSettings: "+getAspectSettings(aspect).length);
449            retval.append("\n       attribute map:");
450            Enumeration<String> keys = aspectAttributeMap.get(aspect).keys();
451            while (keys.hasMoreElements()) {
452                String key = keys.nextElement();
453                retval.append("\n       key: "+key+" value: "+aspectAttributeMap.get(aspect).get(key));
454            }
455        }
456        
457        retval.append("\n  SignalSystem = "+getSignalSystem());
458        
459        return new String(retval);
460    }
461
462    private final static Logger log = LoggerFactory.getLogger(DefaultSignalAppearanceMap.class);
463
464}