001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it 
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011package com.ardor3d.input.logical;
012
013import java.util.List;
014import java.util.Set;
015import java.util.concurrent.CopyOnWriteArraySet;
016
017import com.ardor3d.annotation.GuardedBy;
018import com.ardor3d.annotation.MainThread;
019import com.ardor3d.annotation.ThreadSafe;
020import com.ardor3d.framework.Canvas;
021import com.ardor3d.input.InputState;
022import com.ardor3d.input.PhysicalLayer;
023
024/**
025 * Implementation of a logical layer on top of the physical one, to be able to more easily trigger certain commands for
026 * certain combination of user input.
027 */
028@ThreadSafe
029public final class LogicalLayer {
030    private final Set<InputSource> _inputs = new CopyOnWriteArraySet<>();
031    private final Set<InputTrigger> _triggers = new CopyOnWriteArraySet<>();
032    private LogicalTriggersApplier _applier = new BasicTriggersApplier();
033
034    public LogicalLayer() {}
035
036    public void registerInput(final Canvas source, final PhysicalLayer physicalLayer) {
037        _inputs.add(new InputSource(source, physicalLayer));
038    }
039
040    /**
041     * Register a trigger for evaluation when the {@link #checkTriggers(double)} method is called.
042     * 
043     * @param inputTrigger
044     *            the trigger to check
045     */
046    public void registerTrigger(final InputTrigger inputTrigger) {
047        _triggers.add(inputTrigger);
048    }
049
050    /**
051     * Deregister a trigger for evaluation when the {@link #checkTriggers(double)} method is called.
052     * 
053     * @param inputTrigger
054     *            the trigger to stop checking
055     */
056    public void deregisterTrigger(final InputTrigger inputTrigger) {
057        _triggers.remove(inputTrigger);
058    }
059
060    /**
061     * Check all registered triggers to see if their respective conditions are met. For every trigger whose condition is
062     * true, perform the associated action.
063     * 
064     * @param tpf
065     *            time per frame in seconds
066     */
067    @MainThread
068    public synchronized void checkTriggers(final double tpf) {
069        for (final InputSource is : _inputs) {
070            is.physicalLayer.readState();
071
072            final List<InputState> newStates = is.physicalLayer.drainAvailableStates();
073
074            if (newStates.isEmpty()) {
075                _applier.checkAndPerformTriggers(_triggers, is.source, new TwoInputStates(is.lastState, is.lastState),
076                        tpf);
077            } else {
078                // used to spread tpf evenly among triggered actions
079                final double time = newStates.size() > 1 ? tpf / newStates.size() : tpf;
080                for (final InputState inputState : newStates) {
081                    // no trigger is valid in the LOST_FOCUS state, so don't bother checking them
082                    if (inputState != InputState.LOST_FOCUS) {
083                        _applier.checkAndPerformTriggers(_triggers, is.source, new TwoInputStates(is.lastState,
084                                inputState), time);
085                    }
086
087                    is.lastState = inputState;
088                }
089            }
090        }
091    }
092
093    public void setApplier(final LogicalTriggersApplier applier) {
094        _applier = applier;
095    }
096
097    public LogicalTriggersApplier getApplier() {
098        return _applier;
099    }
100
101    private static class InputSource {
102        private final Canvas source;
103        private final PhysicalLayer physicalLayer;
104        @GuardedBy("LogicalLayer.this")
105        private InputState lastState;
106
107        public InputSource(final Canvas source, final PhysicalLayer physicalLayer) {
108            this.source = source;
109            this.physicalLayer = physicalLayer;
110            lastState = InputState.EMPTY;
111        }
112    }
113
114    public Set<InputTrigger> getTriggers() {
115        return _triggers;
116    }
117
118    public InputTrigger findTriggerById(final String id) {
119        for (final InputTrigger trigger : _triggers) {
120            if (id.equals(trigger.getId())) {
121                return trigger;
122            }
123        }
124        return null;
125    }
126}