BaseFingerprintPositionAndRadioSourceEstimator.java

/*
 * Copyright (C) 2019 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.navigation.indoor.fingerprint;

import com.irurueta.geometry.Point;
import com.irurueta.navigation.LockedException;
import com.irurueta.navigation.NotReadyException;
import com.irurueta.navigation.indoor.RadioSource;
import com.irurueta.navigation.indoor.RadioSourceLocated;
import com.irurueta.navigation.indoor.RssiFingerprint;
import com.irurueta.navigation.indoor.RssiFingerprintLocated;
import com.irurueta.navigation.indoor.RssiReading;

import java.util.List;

/**
 * Base class for position and radio source estimators based on located fingerprints
 * containing only RSSI readings without any prior knowledge of radio sources.
 *
 * @param <P> a {@link Point} type.
 * @param <L> a {@link BaseFingerprintPositionAndRadioSourceEstimatorListener} type.
 */
public abstract class BaseFingerprintPositionAndRadioSourceEstimator<P extends Point<?>,
        L extends BaseFingerprintPositionAndRadioSourceEstimatorListener<?>> {

    /**
     * Default minimum number of nearest fingerprints to search.
     * This is the minimum required value to be used for the estimation of all
     * radio sources and fingerprint position.
     */
    public static final int DEFAULT_MIN_NEAREST_FINGERPRINTS = -1;

    /**
     * Default maximum number of nearest fingerprints to search (no limit).
     */
    public static final int DEFAULT_MAX_NEAREST_FINGERPRINTS = -1;

    /**
     * Default exponent typically used on free space for path loss propagation in
     * terms of distance. This value is used for free space environments.
     */
    public static final double DEFAULT_PATH_LOSS_EXPONENT = 2.0;

    /**
     * Located fingerprints containing RSSI readings.
     */
    protected List<? extends RssiFingerprintLocated<? extends RadioSource,
            ? extends RssiReading<? extends RadioSource>, P>> locatedFingerprints;

    /**
     * Fingerprint containing readings at an unknown location.
     * Readings need to belong to the same radio sources as those given at located
     * fingerprints.
     */
    protected RssiFingerprint<? extends RadioSource,
            ? extends RssiReading<? extends RadioSource>> fingerprint;

    /**
     * Minimum number of nearest fingerprints to search or -1 if there is no limit
     * and all required fingerprints are initially used.
     */
    protected int minNearestFingerprints = DEFAULT_MIN_NEAREST_FINGERPRINTS;

    /**
     * Maximum number of nearest fingerprints to search or -1 if there is no limit and
     * all provided fingerprints are used.
     */
    protected int maxNearestFingerprints = DEFAULT_MAX_NEAREST_FINGERPRINTS;

    /**
     * Path loss exponent to be used by default.
     * This is typically used on free space for path loss propagation in
     * terms of distance.
     * On different environments path loss exponent might have different values:
     * - Free space: 2.0
     * - Urban Area: 2.7 to 3.5
     * - Suburban Area: 3 to 5
     * - Indoor (line-of-sight): 1.6 to 1.8
     */
    protected double pathLossExponent = DEFAULT_PATH_LOSS_EXPONENT;

    /**
     * Listener to be notified of events raised by this instance.
     */
    protected L listener;

    /**
     * Estimated inhomogeneous position coordinates.
     */
    protected double[] estimatedPositionCoordinates;

    /**
     * Radio sources provided along with located fingerprints containing
     * their estimated locations.
     */
    protected List<RadioSourceLocated<P>> estimatedLocatedSources;

    /**
     * Nearest located fingerprints based on their RSSI readings respect to provided fingerprint.
     * These are the fingerprints that are probably located close to the unknown location to be estimated,
     * however their location is approximate due to errors on RSSI readings.
     */
    protected List<RssiFingerprintLocated<RadioSource, RssiReading<RadioSource>, P>> nearestFingerprints;

    /**
     * Indicates if this instance is locked.
     */
    protected boolean locked;

    /**
     * Constructor.
     */
    protected BaseFingerprintPositionAndRadioSourceEstimator() {
    }

    /**
     * Constructor.
     *
     * @param listener listener in charge of handling events.
     */
    protected BaseFingerprintPositionAndRadioSourceEstimator(final L listener) {
        this.listener = listener;
    }

    /**
     * Constructor.
     *
     * @param locatedFingerprints located fingerprints containing RSSI readings.
     * @param fingerprint         fingerprint containing readings at an unknown location
     *                            for provided located fingerprints.
     * @throws IllegalArgumentException if either non located fingerprint or located
     *                                  fingerprints are null.
     */
    protected BaseFingerprintPositionAndRadioSourceEstimator(
            final List<? extends RssiFingerprintLocated<? extends RadioSource,
                    ? extends RssiReading<? extends RadioSource>, P>> locatedFingerprints,
            final RssiFingerprint<? extends RadioSource,
                    ? extends RssiReading<? extends RadioSource>> fingerprint) {
        internalSetLocatedFingerprints(locatedFingerprints);
        internalSetFingerprint(fingerprint);
    }

    /**
     * Constructor.
     *
     * @param locatedFingerprints located fingerprints containing RSSI readings.
     * @param fingerprint         fingerprint containing readings at an unknown location
     *                            for provided located fingerprints.
     * @param listener            listener in charge of handling events.
     * @throws IllegalArgumentException if either non located fingerprint or located
     *                                  fingerprints are null.
     */
    protected BaseFingerprintPositionAndRadioSourceEstimator(
            final List<? extends RssiFingerprintLocated<? extends RadioSource,
                    ? extends RssiReading<? extends RadioSource>, P>> locatedFingerprints,
            final RssiFingerprint<? extends RadioSource,
                    ? extends RssiReading<? extends RadioSource>> fingerprint,
            final L listener) {
        this(listener);
        internalSetLocatedFingerprints(locatedFingerprints);
        internalSetFingerprint(fingerprint);
    }

    /**
     * Gets located fingerprints containing RSSI readings.
     *
     * @return located fingerprints containing RSSI readings.
     */
    public List<RssiFingerprintLocated<RadioSource, RssiReading<RadioSource>, P>> getLocatedFingerprints() {
        //noinspection unchecked
        return (List<RssiFingerprintLocated<RadioSource, RssiReading<RadioSource>,P>>) locatedFingerprints;
    }

    /**
     * Sets located fingerprints containing RSSI readings.
     *
     * @param locatedFingerprints located fingerprints containing RSSI readings.
     * @throws LockedException          if estimator is locked.
     * @throws IllegalArgumentException if provided value is null or there are not enough
     *                                  fingerprints or readings within provided fingerprints (for 2D position estimation at
     *                                  least 2 readings are required in a single fingerprint, or at least 2 fingerprints
     *                                  at different locations containing a single reading are required. For 3D position
     *                                  estimation 3 reading in a single fingerprint, or 3 fingerprints containing a single
     *                                  reading or any combination resulting in at least 3 readings at different locations
     *                                  are required).
     */
    public void setLocatedFingerprints(
            final List<? extends RssiFingerprintLocated<? extends RadioSource,
                    ? extends RssiReading<? extends RadioSource>, P>> locatedFingerprints) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        internalSetLocatedFingerprints(locatedFingerprints);
    }

    /**
     * Gets fingerprint containing readings at an unknown location for provided located
     * fingerprints.
     *
     * @return fingerprint containing readings at an unknown location for provided located
     * fingerprints.
     */
    public RssiFingerprint<RadioSource, RssiReading<RadioSource>> getFingerprint() {
        //noinspection unchecked
        return (RssiFingerprint<RadioSource, RssiReading<RadioSource>>) fingerprint;
    }

    /**
     * Sets fingerprint containing readings at an unknown location for provided located fingerprints.
     *
     * @param fingerprint fingerprint containing readings at an unknown location for provided located fingerprints.
     * @throws LockedException          if estimator is locked.
     * @throws IllegalArgumentException if provided value is null.
     */
    public void setFingerprint(
            final RssiFingerprint<? extends RadioSource, ? extends RssiReading<? extends RadioSource>> fingerprint)
            throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        internalSetFingerprint(fingerprint);
    }

    /**
     * Get minimum number of nearest fingerprints to search.
     *
     * @return minimum number of nearest fingerprints, -1 indicates to initially use
     * all fingerprints needed to estimate available radio sources.
     */
    public int getMinNearestFingerprints() {
        return minNearestFingerprints;
    }

    /**
     * Gets maximum number of nearest fingerprints to search.
     *
     * @return maximum number of nearest fingerprints, -1 indicates to use all available
     * fingerprints.
     */
    public int getMaxNearestFingerprints() {
        return maxNearestFingerprints;
    }

    /**
     * Sets minimum and maximum number of nearest fingerprints to search.
     * If minimum value is -1, then the minimum required number of fingerprints needed
     * to estimate available radio sources is used.
     * If maximum value is -1, then the problem is attempted to be solved until all
     * available fingerprints are used.
     *
     * @param minNearestFingerprints minimum number of nearest fingerprints or -1.
     * @param maxNearestFingerprints maximum number of nearest fingerprints or -1.
     * @throws LockedException          if estimator is locked.
     * @throws IllegalArgumentException if minimum value is larger than maximum value (as
     *                                  long as it has a limit defined), or if maximum value is not negative when
     *                                  minimum one is less than 1, or if minimum value is zero.
     */
    public void setMinMaxNearestFingerprints(
            final int minNearestFingerprints, final int maxNearestFingerprints) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        internalSetMinMaxNearestFingerprints(minNearestFingerprints, maxNearestFingerprints);
    }

    /**
     * Gets path loss exponent to be used by default.
     * This is typically used on free space for path loss propagation in
     * terms of distance.
     * On different environments path loss exponent might have different values:
     * - Free space: 2.0
     * - Urban Area: 2.7 to 3.5
     * - Suburban Area: 3 to 5
     * - Indoor (line-of-sight): 1.6 to 1.8
     *
     * @return path loss exponent to be used by default.
     */
    public double getPathLossExponent() {
        return pathLossExponent;
    }

    /**
     * Sets path loss exponent to be used by default.
     * This is typically used on free space for path loss propagation in
     * terms of distance.
     * On different environments path loss exponent might have different values:
     * - Free space: 2.0
     * - Urban Area: 2.7 to 3.5
     * - Suburban Area: 3 to 5
     * - Indoor (line-of-sight): 1.6 to 1.8
     *
     * @param pathLossExponent path loss exponent to be used by default.
     * @throws LockedException if estimator is locked.
     */
    public void setPathLossExponent(final double pathLossExponent) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        this.pathLossExponent = pathLossExponent;
    }

    /**
     * Gets listener to be notified of events raised by this instance.
     *
     * @return listener to be notified of events raised by this instance.
     */
    public L getListener() {
        return listener;
    }

    /**
     * Sets listener to be notified of events raised by this instance.
     *
     * @param listener listener to be notified of events raised by this instance.
     * @throws LockedException if estimator is locked.
     */
    public void setListener(final L listener) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        this.listener = listener;
    }

    /**
     * Gets estimated inhomogeneous position coordinates.
     *
     * @return estimated inhomogeneous position coordinates.
     */
    public double[] getEstimatedPositionCoordinates() {
        return estimatedPositionCoordinates;
    }

    /**
     * Gets estimated position and stores result into provided instance.
     *
     * @param estimatedPosition instance where estimated position will be stored.
     */
    public void getEstimatedPosition(final P estimatedPosition) {
        if (estimatedPositionCoordinates != null) {
            for (var i = 0; i < estimatedPositionCoordinates.length; i++) {
                estimatedPosition.setInhomogeneousCoordinate(i, estimatedPositionCoordinates[i]);
            }
        }
    }

    /**
     * Gets estimated position or null if not available yet.
     *
     * @return estimated position or null.
     */
    public P getEstimatedPosition() {
        if (estimatedPositionCoordinates == null) {
            return null;
        }

        final var result = createPoint();
        getEstimatedPosition(result);
        return result;
    }

    /**
     * Gets radio sources provided along with located fingerprints containing
     * their estimated locations.
     *
     * @return radio sources containing estimated locations.
     */
    public List<RadioSourceLocated<P>> getEstimatedLocatedSources() {
        return estimatedLocatedSources;
    }

    /**
     * Gets nearest found located fingerprints based on their RSSI readings respect to provided fingerprint.
     * These are the fingerprints that are probably located close to the unknown location to be estimated,
     * however their location is approximate due to errors on RSSI readings.
     *
     * @return nearest located fingerprints based on their RSSI readings or null if estimation has not been done yet.
     */
    public List<RssiFingerprintLocated<RadioSource, RssiReading<RadioSource>, P>> getNearestFingerprints() {
        return nearestFingerprints;
    }

    /**
     * Returns boolean indicating whether this estimator is locked because an estimation
     * is already in progress.
     *
     * @return true if estimator is locked, false otherwise.
     */
    public boolean isLocked() {
        return locked;
    }

    /**
     * Indicates whether estimator is ready to find a solution.
     *
     * @return true if estimator is ready, false otherwise.
     */
    public boolean isReady() {
        return locatedFingerprints != null && fingerprint != null;
    }

    /**
     * Gets number of dimensions of points.
     *
     * @return number of dimensions of points.
     */
    public abstract int getNumberOfDimensions();

    /**
     * Starts estimation of position of unknown fingerprint and position of all radio sources
     * associated to located fingerprints.
     *
     * @throws LockedException                if estimator is locked.
     * @throws NotReadyException              if estimator is not ready.
     * @throws FingerprintEstimationException if estimation fails for some other reason.
     */
    public abstract void estimate() throws LockedException, NotReadyException, FingerprintEstimationException;

    /**
     * Create a point.
     *
     * @return point to be created.
     */
    protected abstract P createPoint();

    /**
     * Internally sets located fingerprints containing RSSI readings.
     *
     * @param locatedFingerprints located fingerprints containing RSSI readings.
     * @throws IllegalArgumentException if provided value is null.
     */
    private void internalSetLocatedFingerprints(
            final List<? extends RssiFingerprintLocated<? extends RadioSource,
                    ? extends RssiReading<? extends RadioSource>, P>> locatedFingerprints) {
        if (locatedFingerprints == null) {
            throw new IllegalArgumentException();
        }

        this.locatedFingerprints = locatedFingerprints;
    }

    /**
     * Internally sets fingerprint containing readings at an unknown location for provided
     * located fingerprints.
     *
     * @param fingerprint fingerprint containing readings at an unknown location for
     *                    provided located fingerprints.
     * @throws IllegalArgumentException if provided value is null.
     */
    private void internalSetFingerprint(
            final RssiFingerprint<? extends RadioSource, ? extends RssiReading<? extends RadioSource>> fingerprint) {
        if (fingerprint == null) {
            throw new IllegalArgumentException();
        }

        this.fingerprint = fingerprint;
    }

    /**
     * Sets minimum and maximum number of nearest fingerprints to search.
     *
     * @param minNearestFingerprints minimum number of nearest fingerprints.
     * @param maxNearestFingerprints maximum number of nearest fingerprints.
     * @throws IllegalArgumentException if minimum value is larger than maximum value (as
     *                                  long as it has a limit defined), or if maximum value is not negative when
     *                                  minimum one is less than 1.
     */
    @SuppressWarnings("Duplicates")
    private void internalSetMinMaxNearestFingerprints(
            final int minNearestFingerprints, final int maxNearestFingerprints) {
        if (minNearestFingerprints == 0 || (minNearestFingerprints < 1 && maxNearestFingerprints >= 0)
                || (maxNearestFingerprints >= 0 && minNearestFingerprints > maxNearestFingerprints)) {
            throw new IllegalArgumentException();
        }

        this.minNearestFingerprints = minNearestFingerprints;
        this.maxNearestFingerprints = maxNearestFingerprints;
    }
}