001package jmri.implementation;
002
003import java.util.Objects;
004import java.util.regex.Matcher;
005import java.util.regex.Pattern;
006
007import javax.annotation.Nonnull;
008
009import jmri.*;
010import jmri.util.PhysicalLocation;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015/**
016 * Extend AbstractReporter for IdTag reporters
017 * <p>
018 * This file is based on {@link jmri.jmrix.rfid.RfidReporter}
019 *
020 * @author Matthew Harris Copyright (c) 2011
021 * @author Paul Bender Copyright (c) 2016, 2019
022 * @since 4.15.3
023 */
024public class AbstractIdTagReporter extends AbstractReporter
025        implements IdTagListener, PhysicalLocationReporter {
026
027    public AbstractIdTagReporter(String systemName) {
028        super(systemName);
029    }
030
031    public AbstractIdTagReporter(String systemName, String userName) {
032        super(systemName, userName);
033    }
034
035    /**
036     * Indicates whether the underlying hardware sends messages determining
037     * when an Id is no longer in this reporter (e.g. RailCom block occupancy exit events).
038     * <p>
039     * When {@code false} (e.g. RFID point detectors), seeing a tag elsewhere implies
040     * it has departed this reporter.
041     * When {@code true}, departure is strictly governed by hardware exit/clear messages.
042     *
043     * @return true if hardware sends explicit exit/departure messages; false otherwise.
044     */
045    public boolean hasExitReports() {
046        return false;
047    }
048
049    private String lastReportString = null;
050
051    /**
052     * {@inheritDoc}
053     */
054    @Override
055    public void notify(IdTag id) {
056        log.debug("Notify: {}", mSystemName);
057        String oldReportString = lastReportString;
058        String newReportString = null;
059        if (id != null) {
060            newReportString = (id instanceof Reportable) ? ((Reportable) id).toReportString() : id.toString();
061        }
062        lastReportString = newReportString;
063        // These are both nullable.
064        if (!Objects.equals(oldReportString, newReportString)) {
065            firePropertyChange(PROPERTY_REPORT_METADATA, oldReportString, newReportString);
066        }
067        if (id != null) {
068            log.debug("Tag {} notified in {}", id, this);
069            // do not update last reporter and last seen if this is an "exit" report
070            // Only happens on LocoNet transponding reports
071            var entryexit = id.getProperty("entryexit");
072            if (entryexit == null || !entryexit.equals("exits")) {
073                Reporter r = id.getWhereLastSeen();
074                id.setWhereLastSeen(this);
075                if (r != null && !r.equals(this)) {
076                    log.trace("{} notifySeenElsewhere on {}", id, r);
077                    r.notifySeenElsewhere(id, this);
078                }
079                log.trace("{} last seen here: {}", id, this.mSystemName);
080            } else {
081                log.trace("{} skipping setWhereLastSeen on {} exits report", this, id);
082            }
083        }
084        setReport(id);
085        setState(id != null ? IdTag.SEEN : IdTag.UNSEEN);
086    }
087
088    /**
089     * {@inheritDoc}
090     */
091    @Override
092    public void setReport(Object r) {
093        super.setReport(r);
094        if (r == null) {
095            lastReportString = null;
096        } else if (r instanceof Reportable) {
097            lastReportString = ((Reportable) r).toReportString();
098        } else {
099            lastReportString = r.toString();
100        }
101    }
102
103    /**
104     * {@inheritDoc}
105     */
106    @Override
107    public void notifySeenElsewhere(IdTag tag, Reporter newReporter) {
108        if (!hasExitReports() && !this.equals(newReporter) && getCurrentReport() == tag) {
109            notify(null);
110        }
111    }
112
113    private int state = UNKNOWN;
114
115    /** {@inheritDoc} */
116    @Override
117    public void setState(int s) {
118        state = s;
119    }
120
121    /** {@inheritDoc} */
122    @Override
123    public int getState() {
124        return state;
125    }
126
127    /** {@inheritDoc} */
128    @Override
129    @Nonnull
130    public String describeState(int state) {
131        switch (state) {
132            case IdTag.SEEN:
133                return Bundle.getMessage("IdTagReporterStateSeen");
134            case IdTag.UNSEEN:
135                return Bundle.getMessage("IdTagReporterStateUnSeen");
136            default:
137                return super.describeState(state);
138        }
139    }
140
141    // Methods to support PhysicalLocationReporter interface
142
143    /**
144     * Get the locomotive address we're reporting about from the current report.
145     * {@inheritDoc}
146     * @param rep ignored, IdTag Reporters don't send String type reports.
147     */
148    @Override
149    public LocoAddress getLocoAddress(String rep) {
150        // For now, we assume the current report.
151        // IdTag.getTagID() is a system-name-ized version of the loco address. I think.
152        // Matcher.group(1) : loco address (I think)
153        Object report = this.getCurrentReport();
154        if (!(report instanceof IdTag)) {
155            // nothing being reported now
156            return null;
157        }
158        IdTag cr = (IdTag) report;
159        ReporterManager rm = InstanceManager.getDefault(jmri.ReporterManager.class);
160        Pattern p = Pattern.compile("" + rm.getSystemPrefix() + rm.typeLetter() + "(\\d+)");
161        Matcher m = p.matcher(cr.getTagID());
162        if (m.find()) {
163            log.debug("Parsed address: {}", m.group(1));
164            // I have no idea what kind of loco address an Ecos reporter uses,
165            // so we'll default to DCC for now.
166            return (new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC));
167        } else {
168            return (null);
169        }
170    }
171
172    /**
173     * Gets the direction (ENTER/EXIT) of the report.
174     * <p>
175     * Because of the way
176     * IdTag Reporters work, all reports are ENTER type.
177     * {@inheritDoc}
178     */
179    @Override
180    public PhysicalLocationReporter.Direction getDirection(String rep) {
181        // TEMPORARY:  Assume we're always Entering, if asked.
182        return (PhysicalLocationReporter.Direction.ENTER);
183    }
184
185    /**
186     * Get the PhysicalLocation of the Reporter
187     *
188     * Reports its own location, for now.
189     * Not sure if that's the right thing or
190     * not. NOT DONE YET
191     *
192     * {@inheritDoc}
193     */
194    @Override
195    public PhysicalLocation getPhysicalLocation() {
196        return (this.getPhysicalLocation(null));
197    }
198
199    /**
200     * Get the PhysicalLocation of the Reporter.
201     *
202     * {@inheritDoc}
203     * @param s unused.
204     */
205    @Override
206    public PhysicalLocation getPhysicalLocation(String s) {
207        return (PhysicalLocation.getBeanPhysicalLocation(this));
208    }
209
210    private static final Logger log = LoggerFactory.getLogger(AbstractIdTagReporter.class);
211
212}