RobustPositionEstimator.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.position;

import com.irurueta.algebra.Matrix;
import com.irurueta.geometry.Point;
import com.irurueta.navigation.LockedException;
import com.irurueta.navigation.NotReadyException;
import com.irurueta.navigation.indoor.Fingerprint;
import com.irurueta.navigation.indoor.RadioSource;
import com.irurueta.navigation.indoor.RadioSourceLocated;
import com.irurueta.navigation.indoor.Reading;
import com.irurueta.navigation.lateration.NonLinearLeastSquaresLaterationSolver;
import com.irurueta.navigation.lateration.RobustLaterationSolver;
import com.irurueta.navigation.lateration.RobustLaterationSolverListener;
import com.irurueta.numerical.robust.InliersData;
import com.irurueta.numerical.robust.RobustEstimatorException;
import com.irurueta.numerical.robust.RobustEstimatorMethod;

import java.util.ArrayList;
import java.util.List;

/**
 * Base class for robust position estimators using located radio sources and their
 * readings at unknown locations.
 * These kind of estimators can be used to robustly determine the position of a given
 * device by getting readings at an unknown location of different radio sources whose
 * locations are known.
 * Implementations of this class should be able to detect and discard outliers in order
 * to find the best solution.
 *
 * @param <P> a {@link Point} type.
 * @param <R> a {@link Reading} type.
 * @param <L> a {@link RobustPositionEstimatorListener} type.
 */
public abstract class RobustPositionEstimator<P extends Point<?>,
        R extends Reading<? extends RadioSource>,
        L extends RobustPositionEstimatorListener<? extends RobustPositionEstimator<?, ?, ?>>> {

    /**
     * Default robust estimator method when none is provided.
     */
    public static final RobustEstimatorMethod DEFAULT_ROBUST_METHOD = RobustEstimatorMethod.PROMEDS;

    /**
     * Indicates that by default located radio source position covariance is taken
     * into account (if available) to determine distance standard deviation.
     */
    public static final boolean DEFAULT_USE_RADIO_SOURCE_POSITION_COVARIANCE = true;

    /**
     * Indicates that by default readings are distributed evenly among radio sources
     * taking into account quality scores of both radio sources and readings.
     */
    public static final boolean DEFAULT_EVENLY_DISTRIBUTE_READINGS = true;

    /**
     * Distance standard deviation assumed for provided distances as a fallback when
     * none can be determined.
     */
    public static final double FALLBACK_DISTANCE_STANDARD_DEVIATION =
            NonLinearLeastSquaresLaterationSolver.DEFAULT_DISTANCE_STANDARD_DEVIATION;

    /**
     * Located radio sources  used for lateration.
     */
    protected List<? extends RadioSourceLocated<P>> sources;

    /**
     * Fingerprint containing readings at an unknown location for provided located
     * radio sources.
     */
    protected Fingerprint<? extends RadioSource, ? extends R> fingerprint;

    /**
     * Indicates whether located radio source position covariances must be taken into
     * account (if available) to determine distance standard deviation.
     */
    private boolean useRadioSourcePositionCovariance = DEFAULT_USE_RADIO_SOURCE_POSITION_COVARIANCE;

    /**
     * Indicates whether readings are evenly distributed among radio sources
     * taking into account quality scores of both radio sources and readings.
     */
    private boolean evenlyDistributeReadings = DEFAULT_EVENLY_DISTRIBUTE_READINGS;

    /**
     * Distance standard deviation fallback value to use when none can be determined
     * from provided radio sources and fingerprint readings.
     */
    private double fallbackDistanceStandardDeviation = FALLBACK_DISTANCE_STANDARD_DEVIATION;

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

    /**
     * A robust lateration solver to solve position.
     */
    protected RobustLaterationSolver<P> laterationSolver;

    /**
     * Listener for the robust lateration solver.
     */
    protected RobustLaterationSolverListener<P> trilaterationSolverListener;

    /**
     * Size of subsets to be checked during robust estimation.
     */
    protected int preliminarySubsetSize;

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

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

    /**
     * Gets located radio sources used for lateration.
     *
     * @return located radio sources used for lateration.
     */
    public List<RadioSourceLocated<P>> getSources() {
        //noinspection unchecked
        return (List<RadioSourceLocated<P>>) sources;
    }

    /**
     * Sets located radio sources used for lateration.
     *
     * @param sources located radio sources used for lateration.
     * @throws LockedException          if estimator is locked.
     * @throws IllegalArgumentException if provided value is null or the number of
     *                                  provided sources is less than the required
     *                                  minimum.
     */
    public void setSources(final List<? extends RadioSourceLocated<P>> sources) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        internalSetSources(sources);
    }

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

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

        internalSetFingerprint(fingerprint);
    }

    /**
     * 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;
    }

    /**
     * Indicates whether located radio source position covariance must be taken into
     * account (if available) to determine distance standard deviation.
     *
     * @return true to take radio source position covariance into account, false
     * otherwise.
     */
    public boolean isRadioSourcePositionCovarianceUsed() {
        return useRadioSourcePositionCovariance;
    }

    /**
     * Specifies whether located radio source position covariance must be taken into
     * account (if available) to determine distance standard deviation.
     *
     * @param useRadioSourcePositionCovariance true to take radio source position
     *                                         covariance into account, false otherwise.
     * @throws LockedException if estimator is locked.
     */
    public void setRadioSourcePositionCovarianceUsed(
            final boolean useRadioSourcePositionCovariance) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        this.useRadioSourcePositionCovariance = useRadioSourcePositionCovariance;

        buildPositionsDistancesDistanceStandardDeviationsAndQualityScores();
    }

    /**
     * Indicates whether readings are evenly distributed among radio sources taking
     * into account quality scores of both radio sources and readings.
     *
     * @return true if readings are evenly distributed, false otherwise.
     */
    public boolean getEvenlyDistributeReadings() {
        return evenlyDistributeReadings;
    }

    /**
     * Specifies whether readings are evenly distributed among radio sources taking
     * into account quality scores of both radio sources and readings.
     *
     * @param evenlyDistributeReadings true if readings are evenly distributed, false
     *                                 otherwise.
     * @throws LockedException if estimator is locked.
     */
    public void setEvenlyDistributeReadings(final boolean evenlyDistributeReadings) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        this.evenlyDistributeReadings = evenlyDistributeReadings;

        buildPositionsDistancesDistanceStandardDeviationsAndQualityScores();
    }

    /**
     * Gets distance standard deviation fallback value to use when none can be
     * determined from provided radio sources and fingerprint readings.
     *
     * @return distance standard deviation to use as fallback.
     */
    public double getFallbackDistanceStandardDeviation() {
        return fallbackDistanceStandardDeviation;
    }

    /**
     * Sets distance standard deviation fallback value to use when none can be
     * determined from provided radio sources and fingerprint readings.
     *
     * @param fallbackDistanceStandardDeviation distance standard deviation to use
     *                                          as fallback.
     * @throws LockedException if estimator is locked.
     */
    public void setFallbackDistanceStandardDeviation(final double fallbackDistanceStandardDeviation)
            throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        this.fallbackDistanceStandardDeviation = fallbackDistanceStandardDeviation;

        buildPositionsDistancesDistanceStandardDeviationsAndQualityScores();
    }

    /**
     * Returns boolean indicating if estimator is locked because estimation is
     * under progress.
     *
     * @return true if estimator is locked, false otherwise.
     */
    public boolean isLocked() {
        return laterationSolver.isLocked();
    }

    /**
     * Returns amount of progress variation before notifying a progress change during
     * estimation.
     *
     * @return amount of progress variation before notifying a progress change during
     * estimation.
     */
    public float getProgressDelta() {
        return laterationSolver.getProgressDelta();
    }

    /**
     * Sets amount of progress variation before notifying a progress change during
     * estimation.
     *
     * @param progressDelta amount of progress variation before notifying a progress
     *                      change during estimation.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if progress delta is less than zero or greater
     *                                  than 1.
     */
    public void setProgressDelta(final float progressDelta) throws LockedException {
        laterationSolver.setProgressDelta(progressDelta);
    }

    /**
     * Returns amount of confidence expressed as a value between 0.0 and 1.0 (which is
     * equivalent to 100%). The amount of confidence indicates the probability that the
     * estimated result is correct. Usually this value will be close to 1.0, but not
     * exactly 1.0.
     *
     * @return amount of confidence as a value between 0.0 and 1.0.
     */
    public double getConfidence() {
        return laterationSolver.getConfidence();
    }

    /**
     * Sets amount of confidence expressed as a value between 0.0 and 1.0 (which is
     * equivalent to 100%). The amount of confidence indicates the probability that the
     * estimated result is correct. Usually this value will be close to 1.0, but not
     * exactly 1.0.
     *
     * @param confidence confidence to be set as a value between 0.0 and 1.0.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is not between 0.0 and 1.0.
     */
    public void setConfidence(final double confidence) throws LockedException {
        laterationSolver.setConfidence(confidence);
    }

    /**
     * Returns maximum allowed number of iterations. If maximum allowed number of
     * iterations is achieved without converging to a result when calling solve(),
     * a RobustEstimatorException will be raised.
     *
     * @return maximum allowed number of iterations.
     */
    public int getMaxIterations() {
        return laterationSolver.getMaxIterations();
    }

    /**
     * Sets maximum allowed number of iterations. When the maximum number of iterations
     * is exceeded, result will not be available, however an approximate result will be
     * available for retrieval.
     *
     * @param maxIterations maximum allowed number of iterations to be set.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is less than 1.
     */
    public void setMaxIterations(final int maxIterations) throws LockedException {
        laterationSolver.setMaxIterations(maxIterations);
    }

    /**
     * Indicates whether result must be refined using a non-linear estimator over found
     * inliers.
     *
     * @return true to refine result, false to simply use result found by robust
     * estimator without further refining.
     */
    public boolean isResultRefined() {
        return laterationSolver.isResultRefined();
    }

    /**
     * Specifies whether result must be refined using a non-linear estimator over found
     * inliers.
     *
     * @param refineResult true to refine result, false to simply use result found by
     *                     robust estimator without further refining.
     * @throws LockedException if this instance is locked.
     */
    public void setResultRefined(final boolean refineResult) throws LockedException {
        laterationSolver.setResultRefined(refineResult);
    }

    /**
     * Indicates whether covariance must be kept after refining result.
     * This setting is only taken into account if result is refined.
     *
     * @return true if covariance must be kept after refining result, false otherwise.
     */
    public boolean isCovarianceKept() {
        return laterationSolver.isCovarianceKept();
    }

    /**
     * Specifies whether covariance must be kept after refining result.
     * This setting is only taken into account if result is refined.
     *
     * @param keepCovariance true if covariance must be kept after refining result,
     *                       false otherwise.
     * @throws LockedException if this instance is locked.
     */
    public void setCovarianceKept(final boolean keepCovariance) throws LockedException {
        laterationSolver.setCovarianceKept(keepCovariance);
    }

    /**
     * Gets initial position to use as a starting point to find a new solution.
     * This is optional, but if provided, when no linear solvers are used, this is
     * taken into account. If linear solvers are used, this is ignored.
     *
     * @return an initial position.
     */
    public P getInitialPosition() {
        return laterationSolver.getInitialPosition();
    }

    /**
     * Sets initial position to use as a starting point to find a new solution.
     * This is optional, but if provided, when no linear solvers are used, this is
     * taken into account. If linear solvers are used, this is ignored.
     *
     * @param initialPosition an initial position.
     * @throws LockedException if this instance is locked.
     */
    public void setInitialPosition(final P initialPosition) throws LockedException {
        laterationSolver.setInitialPosition(initialPosition);
    }

    /**
     * Indicates whether a linear solver is used or not (either homogeneous or
     * inhomogeneous) for preliminary solutions.
     *
     * @return true if a linear solver is used, false otherwise.
     */
    public boolean isLinearSolverUsed() {
        return laterationSolver.isLinearSolverUsed();
    }

    /**
     * Specifies whether a linear solver is used or not (either homogeneous or
     * inhomogeneous) for preliminary solutions.
     *
     * @param linearSolverUsed true if a linear solver is used, false otherwise.
     * @throws LockedException if this instance is locked.
     */
    public void setLinearSolverUsed(final boolean linearSolverUsed) throws LockedException {
        laterationSolver.setLinearSolverUsed(linearSolverUsed);
    }

    /**
     * Indicates whether an homogeneous linear solver is used either to estimate
     * preliminary solutions or an initial solution for preliminary solutions that will
     * be later refined.
     *
     * @return true if homogeneous linear solver is used, false otherwise.
     */
    public boolean isHomogeneousLinearSolverUsed() {
        return laterationSolver.isHomogeneousLinearSolverUsed();
    }

    /**
     * Specifies whether an homogeneous linear solver is used either to estimate
     * preliminary solutions or an initial solution for preliminary solutions that will
     * be later refined.
     *
     * @param useHomogeneousLinearSolver true if homogeneous linear solver is used,
     *                                   false otherwise.
     * @throws LockedException if estimator is locked.
     */
    public void setHomogeneousLinearSolverUsed(final boolean useHomogeneousLinearSolver) throws LockedException {
        laterationSolver.setHomogeneousLinearSolverUsed(useHomogeneousLinearSolver);
    }

    /**
     * Indicates whether preliminary solutions must be refined after an initial linear
     * solution is found.
     * If no initial solution is found using a linear solver, a non-linear solver will
     * be used regardless of this value using an average solution as the initial value
     * to be refined.
     *
     * @return true if preliminary solutions must be refined after an initial linear
     * solution, false otherwise.
     */
    public boolean isPreliminarySolutionRefined() {
        return laterationSolver.isPreliminarySolutionRefined();
    }

    /**
     * Specifies whether preliminary solutions must be refined after an initial linear
     * solution is found.
     * If no initial solution is found using a linear solver, a non-linear solver will
     * be used regardless of this value using an average solution as the initial value
     * to be refined.
     *
     * @param preliminarySolutionRefined true if preliminary solutions must be refined
     *                                   after an initial linear solution, false
     *                                   otherwise.
     * @throws LockedException if estimator is locked.
     */
    public void setPreliminarySolutionRefined(final boolean preliminarySolutionRefined) throws LockedException {
        laterationSolver.setPreliminarySolutionRefined(preliminarySolutionRefined);
    }

    /**
     * Gets data related to inliers found after estimation.
     * Inlier data is related to the internal positions and distances used for
     * solving lateration.
     *
     * @return data related to inliers found after estimation.
     */
    public InliersData getInliersData() {
        return laterationSolver.getInliersData();
    }

    /**
     * Gets known positions of radio sources used internally to solve lateration.
     *
     * @return known positions used internally.
     */
    public P[] getPositions() {
        return laterationSolver.getPositions();
    }

    /**
     * Gets Euclidean distances from known located radio sources to the location of
     * provided readings in a fingerprint.
     * Distance values are used internally to solve lateration.
     *
     * @return Euclidean distances used internally.
     */
    public double[] getDistances() {
        return laterationSolver.getDistances();
    }

    /**
     * Gets standard deviation distances from known located radio sources to the
     * location of provided readings in a fingerprint.
     * Distance standard deviations are used internally to solve lateration.
     *
     * @return standard deviations used internally.
     */
    public double[] getDistanceStandardDeviations() {
        return laterationSolver.getDistanceStandardDeviations();
    }

    /**
     * Indicates whether estimator is ready to find a solution.
     *
     * @return true if estimator is ready, false otherwise.
     */
    public boolean isReady() {
        return laterationSolver.isReady();
    }

    /**
     * Returns quality scores corresponding to each radio source.
     * The larger the score value the better the quality of the sample.
     * This implementation always returns null.
     * Subclasses using quality scores must implement proper behavior.
     *
     * @return quality scores corresponding to each radio source.
     */
    public double[] getSourceQualityScores() {
        return null;
    }

    /**
     * Sets quality scores corresponding to each radio source.
     * The larger the score value the better the quality of the radio source.
     * This implementation makes no action.
     * Subclasses using quality scores must implement proper behavior.
     *
     * @param sourceQualityScores quality scores corresponding to each radio source.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided quality scores length is smaller
     *                                  than minimum required samples.
     */
    public void setSourceQualityScores(final double[] sourceQualityScores) throws LockedException {
    }

    /**
     * Gets quality scores corresponding to each reading within provided fingerprint.
     * The larger the score value the better the quality of the reading.
     * This implementation always returns null.
     * Subclasses using quality scores must implement proper behavior.
     *
     * @return quality scores corresponding to each reading within provided
     * fingerprint.
     */
    public double[] getFingerprintReadingsQualityScores() {
        return null;
    }

    /**
     * Sets quality scores corresponding to each reading within provided fingerprint.
     * The larger the score value the better the quality of the reading.
     * This implementation makes no action.
     * Subclasses using quality scores must implement proper behavior.
     *
     * @param fingerprintReadingsQualityScores quality scores corresponding to each
     *                                         reading within provided fingerprint.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided quality scores length is smaller
     *                                  than minimum required samples.
     */
    public void setFingerprintReadingsQualityScores(final double[] fingerprintReadingsQualityScores)
            throws LockedException {
    }

    /**
     * Gets size of subsets to be checked during robust estimation.
     * This has to be at least {@link #getMinRequiredSources()}.
     *
     * @return size of subsets to be checked during robust estimation.
     */
    public int getPreliminarySubsetSize() {
        return preliminarySubsetSize;
    }

    /**
     * Sets size of subsets to be checked during robust estimation.
     * This has to be at least {@link #getMinRequiredSources()}.
     *
     * @param preliminarySubsetSize size of subsets to be checked during robust estimation.
     * @throws LockedException          if instance is busy solving the lateration problem.
     * @throws IllegalArgumentException if provided value is less than {@link #getMinRequiredSources()}.
     */
    public void setPreliminarySubsetSize(final int preliminarySubsetSize) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        if (preliminarySubsetSize < getMinRequiredSources()) {
            throw new IllegalArgumentException();
        }

        this.preliminarySubsetSize = preliminarySubsetSize;

        buildPositionsDistancesDistanceStandardDeviationsAndQualityScores();
    }

    /**
     * Gets estimated covariance of estimated position if available.
     * This is only available when result has been refined and covariance is kept.
     *
     * @return estimated covariance or null.
     */
    public Matrix getCovariance() {
        return laterationSolver.getCovariance();
    }

    /**
     * Gets estimated position.
     *
     * @return estimated position.
     */
    public P getEstimatedPosition() {
        return laterationSolver.getEstimatedPosition();
    }

    /**
     * Gets number of dimensions of provided points.
     *
     * @return number of dimensions of provided points.
     */
    public int getNumberOfDimensions() {
        return laterationSolver.getNumberOfDimensions();
    }

    /**
     * Estimates position based on provided located radio sources and readings of such
     * sources at an unknown location.
     *
     * @return estimated position.
     * @throws LockedException          if estimator is locked.
     * @throws NotReadyException        if estimator is not ready.
     * @throws RobustEstimatorException if estimation fails for some other reason.
     */
    public P estimate() throws LockedException, NotReadyException, RobustEstimatorException {
        laterationSolver.setPreliminarySubsetSize(preliminarySubsetSize);
        return laterationSolver.solve();
    }

    /**
     * Gets minimum required number of located radio sources to perform lateration.
     *
     * @return minimum required number of located radio sources to perform
     * lateration.
     */
    public abstract int getMinRequiredSources();

    /**
     * Returns method being used for robust estimation.
     *
     * @return method being used for robust estimation.
     */
    public abstract RobustEstimatorMethod getMethod();

    /**
     * Internally sets located radio sources used for lateration.
     *
     * @param sources located radio sources used for lateration.
     * @throws IllegalArgumentException if provided value is null or the number of
     *                                  provided sources is less than the required minimum.
     */
    @SuppressWarnings("Duplicates")
    protected void internalSetSources(final List<? extends RadioSourceLocated<P>> sources) {
        if (sources == null) {
            throw new IllegalArgumentException();
        }

        if (sources.size() < getMinRequiredSources()) {
            throw new IllegalArgumentException();
        }

        this.sources = sources;

        buildPositionsDistancesDistanceStandardDeviationsAndQualityScores();
    }

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

        this.fingerprint = fingerprint;

        buildPositionsDistancesDistanceStandardDeviationsAndQualityScores();
    }

    /**
     * Sets positions, distances and standard deviations of distances on internal
     * lateration solver.
     *
     * @param positions                  positions to be set.
     * @param distances                  distances to be set.
     * @param distanceStandardDeviations standard deviations of distances to be set.
     * @param distanceQualityScores      distance quality scores or null if not required.
     */
    protected abstract void setPositionsDistancesDistanceStandardDeviationsAndQualityScores(
            final List<P> positions, List<Double> distances, final List<Double> distanceStandardDeviations,
            final List<Double> distanceQualityScores);

    /**
     * Builds positions, distances, standard deviation of distances and quality scores
     * for the internal lateration solver.
     */
    @SuppressWarnings("Duplicates")
    protected void buildPositionsDistancesDistanceStandardDeviationsAndQualityScores() {
        if (laterationSolver == null) {
            return;
        }

        final var min = getPreliminarySubsetSize();
        if (sources == null || fingerprint == null || sources.size() < min || fingerprint.getReadings() == null
                || fingerprint.getReadings().size() < min) {
            return;
        }

        final var positions = new ArrayList<P>();
        final var distances = new ArrayList<Double>();
        final var distanceStandardDeviations = new ArrayList<Double>();

        var sourceQualityScores = getSourceQualityScores();
        var fingerprintReadingsQualityScores = getFingerprintReadingsQualityScores();

        if (evenlyDistributeReadings) {
            // distribute evenly by modifying the relative values of quality scores
            if (sourceQualityScores == null) {
                sourceQualityScores = new double[sources.size()];
            }
            if (fingerprintReadingsQualityScores == null) {
                fingerprintReadingsQualityScores = new double[fingerprint.getReadings().size()];
            }

            final var sorter = new ReadingSorter<P, R>(sources, fingerprint, sourceQualityScores,
                    fingerprintReadingsQualityScores);
            sorter.sort();

            final var sortedSources = sorter.getSortedSourcesAndReadings();

            var j = 0;
            var k = 0;
            boolean finished;
            do {
                var i = 0;
                finished = true;
                for (final var sortedSource : sortedSources) {
                    sourceQualityScores[sortedSource.position] = i;
                    i--;

                    final var sortedReadings = sortedSource.readingsWithQualityScores;
                    if (k < sortedReadings.size()) {
                        finished = false;
                        ReadingSorter.ReadingWithQualityScore<R> sortedReading = sortedReadings.get(k);

                        fingerprintReadingsQualityScores[sortedReading.position] = j;
                        j--;
                    }
                }
                k++;
            } while (!finished);
        }

        List<Double> distanceQualityScores = null;
        if (sourceQualityScores != null || fingerprintReadingsQualityScores != null) {
            distanceQualityScores = new ArrayList<>();
        }
        PositionEstimatorHelper.buildPositionsDistancesDistanceStandardDeviationsAndQualityScores(
                sources, fingerprint, sourceQualityScores, fingerprintReadingsQualityScores,
                isRadioSourcePositionCovarianceUsed(), getFallbackDistanceStandardDeviation(), positions, distances,
                distanceStandardDeviations, distanceQualityScores);

        setPositionsDistancesDistanceStandardDeviationsAndQualityScores(positions, distances,
                distanceStandardDeviations, distanceQualityScores);
    }
}