ReadingSorter.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.geometry.Point;
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.indoor.ReadingType;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

/**
 * Evenly sorts readings of a fingerprint among different radio sources taking into
 * account their respective quality scores as well.
 *
 * @param <P> a {@link Point} type.
 * @param <R> a {@link Reading} type.
 */
public class ReadingSorter<P extends Point<?>, R extends Reading<? extends RadioSource>> {

    /**
     * Sources to be taken into account within readings.
     */
    private final List<? extends RadioSourceLocated<P>> sources;

    /**
     * Fingerprint containing readings of different sources.
     */
    private final Fingerprint<? extends RadioSource, ? extends R> fingerprint;

    /**
     * Quality scores associated to each radio source.
     */
    private final double[] sourceQualityScores;

    /**
     * Quality scores associated to each reading within the fingerprint.
     */
    private final double[] fingerprintReadingsQualityScores;

    /**
     * Contains sorted sources and readings.
     */
    private List<RadioSourceSourceWithQualityScore<P, R>> sortedSourcesAndReadings;

    /**
     * Constructor.
     *
     * @param sources                          sources to take into account within
     *                                         readings.
     * @param fingerprint                      fingerprint containing readings of
     *                                         different sources.
     * @param sourceQualityScores              quality scores associated to each radio
     *                                         source.
     * @param fingerprintReadingsQualityScores quality scores associated to each reading
     *                                         within the fingerprint.
     * @throws IllegalArgumentException if number of source quality scores is not equal
     *                                  to the number of sources, or if number of
     *                                  fingerprint reading quality scores is not equal
     *                                  to the number of readings within fingerprint.
     */
    ReadingSorter(final List<? extends RadioSourceLocated<P>> sources,
                  final Fingerprint<? extends RadioSource, ? extends R> fingerprint,
                  final double[] sourceQualityScores, final double[] fingerprintReadingsQualityScores) {
        if (sources.size() != sourceQualityScores.length) {
            throw new IllegalArgumentException();
        }

        if (fingerprint.getReadings().size() != fingerprintReadingsQualityScores.length) {
            throw new IllegalArgumentException();
        }

        this.sources = sources;
        this.fingerprint = fingerprint;
        this.sourceQualityScores = sourceQualityScores;
        this.fingerprintReadingsQualityScores = fingerprintReadingsQualityScores;
    }

    /**
     * Gets sources to be taken into account within readings.
     *
     * @return sources to be taken into account within readings.
     */
    public List<RadioSourceLocated<P>> getSources() {
        //noinspection unchecked
        return (List<RadioSourceLocated<P>>) sources;
    }

    /**
     * Gets fingerprint containing readings of different sources.
     *
     * @return fingerprint containing readings of different sources.
     */
    public Fingerprint<RadioSource, Reading<RadioSource>> getFingerprint() {
        //noinspection unchecked
        return (Fingerprint<RadioSource, Reading<RadioSource>>) fingerprint;
    }

    /**
     * Gets quality scores associated to each radio source.
     *
     * @return quality scores associated to each radio source.
     */
    double[] getSourceQualityScores() {
        return sourceQualityScores;
    }

    /**
     * Gets quality scores associated to each reading within the fingerprint.
     *
     * @return quality scores associated to each reading within the fingerprint.
     */
    double[] getFingerprintReadingsQualityScores() {
        return fingerprintReadingsQualityScores;
    }

    /**
     * Sorts readings with quality scores.
     */
    void sort() {

        final var sourcesMap = new HashMap<RadioSourceLocated<P>, RadioSourceSourceWithQualityScore<P, R>>();

        // build sources
        final var sourcesWithQualityScores = new ArrayList<RadioSourceSourceWithQualityScore<P, R>>();
        var sourcePosition = 0;
        for (final var source : sources) {
            final var sourceWithQualityScore = new RadioSourceSourceWithQualityScore<P, R>();
            sourceWithQualityScore.source = source;
            sourceWithQualityScore.qualityScore = sourceQualityScores[sourcePosition];
            sourceWithQualityScore.position = sourcePosition;
            sourceWithQualityScore.readingsWithQualityScores = new ArrayList<>();

            sourcesWithQualityScores.add(sourceWithQualityScore);

            sourcesMap.put(source, sourceWithQualityScore);

            sourcePosition++;
        }

        // build readings
        var readingPosition = 0;
        for (@SuppressWarnings("unchecked") final var reading : fingerprint.getReadings()) {
            if (!(reading.getSource() instanceof RadioSourceLocated)) {
                continue;
            }

            //noinspection unchecked
            final var radioSourceLocated = (RadioSourceLocated<P>) reading.getSource();
            final var sourceWithQualityScore = sourcesMap.get(radioSourceLocated);

            if (sourceWithQualityScore == null) {
                continue;
            }

            final var readingsWithQualityScores = sourceWithQualityScore.readingsWithQualityScores;

            final var readingWithQualityScore = new ReadingWithQualityScore<R>();
            readingWithQualityScore.reading = reading;
            readingWithQualityScore.qualityScore = fingerprintReadingsQualityScores[readingPosition];
            readingWithQualityScore.position = readingPosition;

            readingsWithQualityScores.add(readingWithQualityScore);

            readingPosition++;
        }

        // sort all readings within sources
        for (final var sourceWithQualityScore : sourcesWithQualityScores) {
            // sort all readings for this source from highest to lowest quality

            // noinspection unchecked
            final ReadingWithQualityScore<R>[] readingsWithQualityScoresArray =
                    new ReadingWithQualityScore[sourceWithQualityScore.readingsWithQualityScores.size()];
            sourceWithQualityScore.readingsWithQualityScores.toArray(readingsWithQualityScoresArray);
            Arrays.sort(readingsWithQualityScoresArray, new ReadingComparator<>());

            sourceWithQualityScore.readingsWithQualityScores = Arrays.asList(readingsWithQualityScoresArray);
        }

        // sort all sources from highest to lowest quality

        // noinspection unchecked
        final RadioSourceSourceWithQualityScore<P, R>[] sourcesWithQualityScoresArray =
                new RadioSourceSourceWithQualityScore[sourcesWithQualityScores.size()];
        sourcesWithQualityScores.toArray(sourcesWithQualityScoresArray);
        Arrays.sort(sourcesWithQualityScoresArray, new RadioSourceComparator<>());

        sortedSourcesAndReadings = Arrays.asList(sourcesWithQualityScoresArray);
    }

    /**
     * Gets sorted sources and readings.
     *
     * @return sorted sources and readings.
     */
    List<RadioSourceSourceWithQualityScore<P, R>> getSortedSourcesAndReadings() {
        return sortedSourcesAndReadings;
    }

    /**
     * Contains a reading with its associated quality score.
     *
     * @param <R> a {@link Reading} type.
     */
    static class ReadingWithQualityScore<R extends Reading<? extends RadioSource>> {
        /**
         * A reading.
         */
        R reading;

        /**
         * Reading quality score.
         */
        double qualityScore;

        /**
         * Original position.
         */
        int position;
    }

    /**
     * Contains a radio source with its associated quality score.
     *
     * @param <P> a {@link Point} type.
     * @param <R> a {@link Reading} type.
     */
    static class RadioSourceSourceWithQualityScore<P extends Point<?>, R extends Reading<? extends RadioSource>> {
        /**
         * Radio source.
         */
        RadioSourceLocated<P> source;

        /**
         * Radio source quality score.
         */
        double qualityScore;

        /**
         * Original position.
         */
        int position;

        List<ReadingWithQualityScore<R>> readingsWithQualityScores;
    }

    /**
     * Comparator to order readings based on their quality scores.
     *
     * @param <R> a {@link Reading} type.
     */
    private static class ReadingComparator<R extends Reading<? extends RadioSource>> implements
            Comparator<ReadingWithQualityScore<R>> {

        /**
         * Orders readings so that readings with higher scores go first.
         *
         * @param o1 1st item to be compared.
         * @param o2 2nd item to be compared.
         * @return -1 if first item goes first, 0 if both are equal and 1 if first item
         * goes second.
         */
        @Override
        public int compare(final ReadingWithQualityScore<R> o1, final ReadingWithQualityScore<R> o2) {

            // Take reading type into account so that ranging readings go first,
            // then ranging+RSSI and finally RSSI readings.
            if (o1.reading.getType() != o2.reading.getType()) {
                switch (o1.reading.getType()) {
                    case RANGING_READING:
                        // o1 goes first (o2 last)
                        return -1;

                    case RANGING_AND_RSSI_READING:
                        if (o2.reading.getType() == ReadingType.RANGING_READING) {
                            // RANGING_READING
                            // o2 goes first (o1 last)
                            return 1;
                        } else {
                            // RSSI_READING
                            // o1 goes first (o2 last)
                            return -1;
                        }
                    case RSSI_READING:
                    default:
                        // o2 goes first (o1 last)
                        return 1;
                }
            }

            // same reading type

            // order by reading quality score
            return Double.compare(o2.qualityScore, o1.qualityScore);
        }
    }

    /**
     * Comparator to order radio sources based on their quality scores.
     *
     * @param <P> a {@link Point} type.
     * @param <R> a {@link Reading} type.
     */
    private static class RadioSourceComparator<P extends Point<?>, R extends Reading<? extends RadioSource>> implements
            Comparator<RadioSourceSourceWithQualityScore<P, R>> {

        /**
         * Orders radio sources so that radio sources with higher scores go first.
         *
         * @param o1 1st item to be compared.
         * @param o2 2nd item to be compared.
         * @return -1 if first item goes first, 0 if both are equal and 1 if first item
         * goes second.
         */
        @Override
        public int compare(final RadioSourceSourceWithQualityScore<P, R> o1,
                           final RadioSourceSourceWithQualityScore<P, R> o2) {
            return Double.compare(o2.qualityScore, o1.qualityScore);
        }
    }
}