uk.ac.horizon.artcodes.detect.handler.MultipleMarkerActionDetectionHandler.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.horizon.artcodes.detect.handler.MultipleMarkerActionDetectionHandler.java

Source

/*
 * Artcodes recognises a different marker scheme that allows the
 * creation of aesthetically pleasing, even beautiful, codes.
 * Copyright (C) 2013-2016  The University of Nottingham
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Affero General Public License as published
 *     by the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Affero General Public License for more details.
 *
 *     You should have received a copy of the GNU Affero General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package uk.ac.horizon.artcodes.detect.handler;

import android.graphics.Bitmap;
import android.util.Log;

import org.opencv.android.Utils;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Rect;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import uk.ac.horizon.artcodes.detect.handler.ActionDetectionHandler;
import uk.ac.horizon.artcodes.detect.handler.MarkerCodeDetectionHandler;
import uk.ac.horizon.artcodes.detect.marker.Marker;
import uk.ac.horizon.artcodes.detect.marker.MarkerWithEmbeddedChecksum;
import uk.ac.horizon.artcodes.drawer.MarkerDrawer;
import uk.ac.horizon.artcodes.model.Action;
import uk.ac.horizon.artcodes.model.Experience;
import uk.ac.horizon.artcodes.model.MarkerImage;

/**
 * Detects single marker, group and sequence actions. Provides images of detected markers and
 * indicates possible future actions based on the current state.
 *
 * Consider this a reference implementation that could be improved.
 */
public class MultipleMarkerActionDetectionHandler extends MarkerCodeDetectionHandler {
    /**
     * A class that holds the details of a marker's detection state
     */
    private static class MarkerDetectionRecord {
        static int instanceCount = 0;
        final int instanceId;
        final String code;
        final Marker marker;
        long firstDetected;
        long lastDetected;
        int count;
        MarkerImage markerImage;

        public MarkerDetectionRecord(Marker marker) {
            this.marker = marker;
            this.code = marker.toString();
            this.count = 0;
            this.instanceId = instanceCount++;
        }

        public MarkerDetectionRecord clone(Marker marker) {
            MarkerDetectionRecord clone = new MarkerDetectionRecord(marker == null ? this.marker : marker);
            clone.firstDetected = this.firstDetected;
            clone.lastDetected = this.lastDetected;
            clone.count = this.count;
            clone.markerImage = this.markerImage;
            return clone;
        }

        @Override
        public int hashCode() {
            return this.instanceId;
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof MarkerDetectionRecord) {
                return this.instanceId == ((MarkerDetectionRecord) o).instanceId;
            }
            return false;
        }

        @Override
        public String toString() {
            return "<#" + instanceId + " " + code + " x" + count + ">";
        }
    }

    protected final ActionDetectionHandler markerActionHandler;
    protected final Experience experience;
    protected final MarkerDrawer markerDrawer;

    protected static final int REQUIRED = 5;
    protected static final int MAX = REQUIRED * 4;

    protected long lastAddedToHistory = 0;
    protected boolean shouldClearHistoryOnReset = true;

    protected List<MarkerDetectionRecord> mDetectionHistory = new ArrayList<>();
    protected List<String> mCodesDetected = new ArrayList<>();
    protected Map<String, MarkerDetectionRecord> mActiveMarkerRecoreds = new HashMap<>();

    public MultipleMarkerActionDetectionHandler(ActionDetectionHandler markerActionHandler, Experience experience,
            MarkerDrawer markerDrawer) {
        super(experience, null);
        this.markerActionHandler = markerActionHandler;
        this.experience = experience;
        this.markerDrawer = markerDrawer;
    }

    @Override
    public void onMarkersDetected(Collection<Marker> markers, ArrayList<MatOfPoint> contours, Mat hierarchy,
            Size sourceImageSize) {
        addMarkers(markers, contours, hierarchy, sourceImageSize);
        actOnMarkers();
    }

    private MarkerImage createImageForMarker(Marker marker, ArrayList<MatOfPoint> contours, Mat hierarchy,
            Size sourceImageSize) {
        if (marker != null) {
            final Rect boundingRect = Imgproc.boundingRect(contours.get(marker.markerIndex));
            final Mat thumbnailMat = this.markerDrawer.drawMarker(marker, contours, hierarchy, boundingRect, null);
            final Bitmap thumbnail = Bitmap.createBitmap(thumbnailMat.width(), thumbnailMat.height(),
                    Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(thumbnailMat, thumbnail);
            return new MarkerImage(marker.toString(), thumbnail,
                    (float) (boundingRect.tl().x / sourceImageSize.width),
                    (float) (boundingRect.tl().y / sourceImageSize.height),
                    (float) (boundingRect.width / sourceImageSize.width),
                    (float) (boundingRect.height / sourceImageSize.height));
        }
        return null;
    }

    public void reset() {
        mActiveMarkerRecoreds.clear();
        mCodesDetected.clear();
        if (shouldClearHistoryOnReset) {
            mDetectionHistory.clear();
        }
        existingAction = null;
        existingThumbnails = null;
        existingFutureAction = null;
        this.markerActionHandler.onMarkerActionDetected(null, null, null);
    }

    public void addMarkers(Collection<Marker> markers, ArrayList<MatOfPoint> contours, Mat hierarchy,
            Size sourceImageSize) {
        long time = System.currentTimeMillis();

        // Process markers detected on this frame
        for (Marker marker : markers) {
            String code = marker.toString();

            MarkerDetectionRecord markerDetectionRecord = mActiveMarkerRecoreds.get(code);
            if (markerDetectionRecord == null) {
                // New marker: add it to data structure
                markerDetectionRecord = new MarkerDetectionRecord(marker);
                mActiveMarkerRecoreds.put(code, markerDetectionRecord);
            }

            int countIncrease = marker instanceof MarkerWithEmbeddedChecksum ? REQUIRED - 1 : 1;
            // add to history (if it has passed the required count on this frame)
            if (markerDetectionRecord.count < REQUIRED && markerDetectionRecord.count + countIncrease >= REQUIRED) {
                // don't add duplicates to history unless enough time has passed
                if (this.mDetectionHistory.isEmpty() || System.currentTimeMillis() - this.lastAddedToHistory >= 1000
                        || !code.equals(this.mDetectionHistory.get(this.mDetectionHistory.size() - 1).code)) {
                    if (markerDetectionRecord.markerImage != null) {
                        // if second time this marker is detected
                        // create new entry and leave old one in history
                        markerDetectionRecord.markerImage.newDetection = false;
                        markerDetectionRecord.markerImage.detectionActive = false;
                        markerDetectionRecord = markerDetectionRecord.clone(marker);
                        mActiveMarkerRecoreds.put(code, markerDetectionRecord);
                    }
                    markerDetectionRecord.firstDetected = time;
                    mDetectionHistory.add(markerDetectionRecord);
                    this.lastAddedToHistory = time;
                    mCodesDetected.add(markerDetectionRecord.code);
                }
                markerDetectionRecord.markerImage = createImageForMarker(marker, contours, hierarchy,
                        sourceImageSize);
                markerDetectionRecord.markerImage.newDetection = true;
            } else if (markerDetectionRecord.markerImage != null) {
                markerDetectionRecord.markerImage.newDetection = false;
            }

            // increase its count
            markerDetectionRecord.count = Math.min(markerDetectionRecord.count + countIncrease, MAX);
            markerDetectionRecord.lastDetected = time;
        }

        // Workout which markers have timed out:
        List<String> toRemove = new ArrayList<>();
        for (MarkerDetectionRecord markerRecord : mActiveMarkerRecoreds.values()) {
            if (!markers.contains(markerRecord.marker)) {
                if (markerRecord.count == REQUIRED) {
                    mCodesDetected.remove(markerRecord.code);
                    if (markerRecord.markerImage != null) {
                        markerRecord.markerImage.detectionActive = false;
                        markerRecord.markerImage.newDetection = false;
                    }
                } else if (markerRecord.count <= 1) {
                    toRemove.add(markerRecord.code);
                    continue;
                }
                markerRecord.count = markerRecord.count - 1;
            }
        }
        for (String markerToRemove : toRemove) {
            mActiveMarkerRecoreds.remove(markerToRemove);
        }
        Collections.sort(mCodesDetected);
    }

    private void actOnMarkers() {
        final String standardCode = getStandardCode();
        final Action action = getActionFor(standardCode);

        final Action sequentialAction = getActionFor(getSequentialCode());
        final Action futureSequentialAction = getPossibleFutureSequentialActionFor(
                sequentialAction == null ? action : sequentialAction, standardCode);
        if (sequentialAction != null) {
            sendIfResultChanged(sequentialAction, futureSequentialAction,
                    getImagesForAction(futureSequentialAction));
            return;
        }

        final Action groupAction = getActionFor(getGroupCode());
        final Action futureGroupAction = getPossibleFutureGroupActionFor(
                groupAction == null ? action : groupAction);
        if (groupAction != null) {
            sendIfResultChanged(groupAction, futureGroupAction, getImagesForAction(futureGroupAction));
            return;
        }

        final Action futureAction = futureSequentialAction != action ? futureSequentialAction : futureGroupAction;
        sendIfResultChanged(action, futureAction, getImagesForAction(futureAction));
    }

    private Action existingAction = null, existingFutureAction = null;
    private List<MarkerImage> existingThumbnails = null;

    private void sendIfResultChanged(Action action, Action futureAction, List<MarkerImage> thumbnails) {
        if (((existingAction != null && !existingAction.equals(action))
                || (action != null && !action.equals(existingAction)))
                || ((existingThumbnails != null && !existingThumbnails.equals(thumbnails))
                        || (thumbnails != null && !thumbnails.equals(existingThumbnails)))
                || ((existingFutureAction != null && !existingFutureAction.equals(futureAction))
                        || ((futureAction != null && !futureAction.equals(existingFutureAction))))) {
            this.existingAction = action;
            this.existingThumbnails = thumbnails;
            this.existingFutureAction = futureAction;
            this.markerActionHandler.onMarkerActionDetected(action, futureAction, thumbnails);
        }

    }

    private List<MarkerImage> getImagesForAction(Action action) {
        if (action != null) {
            List<MarkerImage> result = new ArrayList<>(action.getCodes().size());
            if (action.getMatch() == Action.Match.any) {
                for (String code : action.getCodes()) {
                    MarkerDetectionRecord record = mActiveMarkerRecoreds.get(code);
                    if (record != null && record.markerImage != null && record.markerImage.detectionActive) {
                        result.add(record.markerImage);
                        return result;
                    }
                }
            } else if (action.getMatch() == Action.Match.all) {
                for (String code : action.getCodes()) {
                    MarkerDetectionRecord record = mActiveMarkerRecoreds.get(code);
                    if (record != null && record.markerImage != null && record.markerImage.detectionActive) {
                        result.add(record.markerImage);
                    } else {
                        result.add(null);
                    }
                }
                return result;
            } else if (action.getMatch() == Action.Match.sequence) {
                List<String> historyAsStrings = new ArrayList<>();
                for (MarkerDetectionRecord record : mDetectionHistory) {
                    historyAsStrings.add(record.code);
                }

                for (int numberOfCodesInHistory = Math.min(action.getCodes().size(),
                        historyAsStrings.size()); numberOfCodesInHistory > 0; --numberOfCodesInHistory) {
                    if (firstN(action.getCodes(), numberOfCodesInHistory)
                            .equals(lastN(historyAsStrings, numberOfCodesInHistory))) {
                        int start = mDetectionHistory.size() - numberOfCodesInHistory;
                        for (MarkerDetectionRecord record : mDetectionHistory.subList(start < 0 ? 0 : start,
                                mDetectionHistory.size())) {
                            result.add(record.markerImage);
                        }
                        for (int i = numberOfCodesInHistory; i < action.getCodes().size(); ++i) {
                            result.add(null);
                        }
                        break;
                    }
                }
                return result;
            }
            return result;
        }
        return null;
    }

    private List<String> firstN(List<String> list, int n) {
        int start = list.size() - n;
        return list.subList(0, n > list.size() ? list.size() : n);
    }

    private List<String> lastN(List<String> list, int n) {
        int start = list.size() - n;
        return list.subList(start < 0 ? 0 : start, list.size());
    }

    /**
     * Search for group codes (or "pattern groups") in the detected codes. This will only return
     * group codes set in the experience.
     * @return
     */
    private String getGroupCode() {
        if (experience != null) {
            // Search for pattern groups
            // By getting every combination of the currently detected markers and checking if they exist in the experience (biggest groups first, groups must include at least 2 markers)
            if (mCodesDetected != null && mCodesDetected.size() > 1) {
                List<Set<List<String>>> combinations = new ArrayList<>();
                combinationsOfStrings(mCodesDetected, mCodesDetected.size(), combinations);
                for (int i = combinations.size() - 1; i >= 1; --i) {
                    List<String> mostRecentGroup = null;
                    String mostRecentGroupStr = null;
                    for (List<String> code : combinations.get(i)) {
                        String codeStr = joinStr(code, "+");
                        if (isValidCode(codeStr) && doMarkerDetectionTimesOverlap(code)
                                && getMostRecentDetectionTime(code,
                                        mostRecentGroup) > getMostRecentDetectionTime(mostRecentGroup, code)) {
                            mostRecentGroup = code;
                            mostRecentGroupStr = codeStr;
                        }
                    }
                    if (mostRecentGroup != null) {
                        return mostRecentGroupStr;
                    }
                }
            }
        }
        return null;
    }

    private long getMostRecentDetectionTime(List<String> codes, List<String> excluding) {
        long mostRecentTime = 0;
        if (codes != null) {
            for (String codeStr : codes) {
                if (excluding == null || !excluding.contains(codeStr)) {
                    MarkerDetectionRecord code = mActiveMarkerRecoreds.get(codeStr);
                    if (code != null && code.lastDetected > mostRecentTime) {
                        mostRecentTime = code.lastDetected;
                    }
                }
            }
        }
        return mostRecentTime;
    }

    private boolean doMarkerDetectionTimesOverlap(List<String> codes) {
        for (int i = 0; i < codes.size() - 1; ++i) {
            MarkerDetectionRecord code1 = mActiveMarkerRecoreds.get(codes.get(i));
            boolean overlapFound = false;
            for (int j = i + 1; j < codes.size(); ++j) {
                MarkerDetectionRecord code2 = mActiveMarkerRecoreds.get(codes.get(j));
                overlapFound = doTimesOverlap(code1.firstDetected, code1.lastDetected, code2.firstDetected,
                        code2.lastDetected);
                if (overlapFound) {
                    break;
                }
            }
            if (!overlapFound) {
                return false;
            }
        }
        return true;
    }

    private static boolean doTimesOverlap(long firstDetected1, long lastDetected1, long firstDetected2,
            long lastDetected2) {
        return (firstDetected1 <= lastDetected2) && (lastDetected1 >= firstDetected2);
    }

    /**
     * Get all the combinations of objects up to a maximum size for the combination and add it to the result List.
     * E.g. combinationsOfStrings([1,3,2], 2, []) changes the result array to [([1],[2],[3]),([1,2],[1,3],[2,3])] where () denotes a Set and [] denotes a List.
     */
    private static void combinationsOfStrings(List<String> strings, int maxCombinationSize,
            List<Set<List<String>>> result) {

        if (maxCombinationSize > 0) {
            if (maxCombinationSize == 1) {
                Set<List<String>> resultForN = new HashSet<>();
                for (String code : strings) {
                    List<String> tmp = new ArrayList<>();
                    tmp.add(code);
                    resultForN.add(tmp);
                }
                result.add(resultForN);
            } else if (maxCombinationSize == strings.size()) {
                combinationsOfStrings(strings, maxCombinationSize - 1, result);
                Set<List<String>> resultForN = new HashSet<>();
                Collections.sort(strings);
                resultForN.add(strings);
                result.add(resultForN);
            } else {
                Set<List<String>> resultForN = new HashSet<>();
                combinationsOfStrings(strings, maxCombinationSize - 1, result);
                Set<List<String>> base = result.get(result.size() - 1);

                for (String code : strings) {
                    for (List<String> setMinus1 : base) {
                        if (!setMinus1.contains(code)) {
                            List<String> aResult = new ArrayList<>(setMinus1);
                            aResult.add(code);
                            Collections.sort(aResult);
                            resultForN.add(aResult);
                        }
                    }
                }

                result.add(resultForN);
            }
        }
    }

    /**
     * Search for sequential codes (or "pattern paths") in detection history. This method may
     * remove items from history that do not match the beginning of any sequential code in the
     * experience and will only return a code from the experience.
     * @return
     */
    private String getSequentialCode() {
        if (experience != null) {
            // Search for sequential actions in history
            // by creating history sub-lists and checking if any codes in the experience match.
            // e.g. if history=[A,B,C,D] check sub-lists [A,B,C,D], [B,C,D], [C,D].
            if (mDetectionHistory != null && mDetectionHistory.size() > 0) {
                boolean foundPrefix = false;
                int start = 0;

                List<String> detectionHistoryAsStrings = new ArrayList<>();
                for (MarkerDetectionRecord record : mDetectionHistory) {
                    detectionHistoryAsStrings.add(record.code);
                }
                while (start < mDetectionHistory.size()) {
                    List<String> subList = detectionHistoryAsStrings.subList(start,
                            detectionHistoryAsStrings.size());
                    String joinedString = joinStr(subList, ">");
                    if (subList.size() != 1 && isValidCode(joinedString)) {
                        // Case 1: The history sublist is a sequential code in the experience.
                        return joinedString;
                    } else if (!foundPrefix && !hasSequentialPrefix(joinedString)) {
                        // Case 2: No sequential codes in the experience start with the history sublist (as well as previous history sublists).
                        // So remove the first part of it from history
                        // This ensures that history never grows longer than the longest code
                        detectionHistoryAsStrings.remove(0);
                        mDetectionHistory.remove(0);
                        start = 0;
                    } else {
                        // Case 3: Sequential codes in the experience start with the history sublist (or a previous history sublist).
                        foundPrefix = true;
                        start++;
                    }
                }
            }
        }
        return null;
    }

    private static String joinStr(Collection<String> strings, String joiner) {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String string : strings) {
            if (!first) {
                sb.append(joiner);
            }
            sb.append(string);
            first = false;
        }
        return sb.toString();
    }

    /**
     * Search for the single marker with the highest count that is in the experience, or just the highest count if none are in the experience.
     * @return
     */
    private String getStandardCode() {
        MarkerDetectionRecord result = null;
        boolean resultIsInExperience = false;
        for (String code : mCodesDetected) {
            MarkerDetectionRecord marker = mActiveMarkerRecoreds.get(code);
            boolean markerIsInExperience = isValidCode(code);
            if (result == null || (!resultIsInExperience && markerIsInExperience)
                    || (resultIsInExperience == markerIsInExperience && ((marker.lastDetected > result.lastDetected)
                            || (marker.lastDetected == result.lastDetected
                                    && marker.firstDetected > result.firstDetected)
                            || (marker.lastDetected == result.lastDetected
                                    && marker.firstDetected == result.firstDetected
                                    && marker.count > result.count)))) {
                result = marker;
                resultIsInExperience = markerIsInExperience;
            }
        }

        return result == null ? null : result.code;
    }

    private HashMap<String, Action> validCodes = null;
    private HashMap<String, Set<Action>> subGroupCodes = null;
    private HashMap<String, Set<Action>> subSequenceCodes = null;

    private void logDataCache() {
        Log.i("DATACACHE", "Valid codes = " + joinStr(validCodes.keySet(), ", "));

        List<String> subGroupCodesStrs = new ArrayList<>();
        for (Map.Entry<String, Set<Action>> entry : subGroupCodes.entrySet()) {
            List<String> actionStrs = new ArrayList<>();
            for (Action action : entry.getValue()) {
                actionStrs.add(joinStr(action.getCodes(), ","));
            }
            subGroupCodesStrs.add(entry.getKey() + ": " + joinStr(actionStrs, " or "));
        }
        Log.i("DATACACHE", "Sub-group codes = " + joinStr(subGroupCodesStrs, ", "));

        subGroupCodesStrs = new ArrayList<>();
        for (Map.Entry<String, Set<Action>> entry : subSequenceCodes.entrySet()) {
            List<String> actionStrs = new ArrayList<>();
            for (Action action : entry.getValue()) {
                actionStrs.add(joinStr(action.getCodes(), ","));
            }
            subGroupCodesStrs.add(entry.getKey() + ": " + joinStr(actionStrs, " or "));
        }
        Log.i("DATACACHE", "Sub-sequence codes = " + joinStr(subGroupCodesStrs, ", "));
    }

    private boolean isValidCode(String code) {
        if (validCodes == null) {
            createDataCache();
        }
        return validCodes.containsKey(code);
    }

    private boolean hasSequentialPrefix(String prefix) {
        if (subSequenceCodes == null) {
            createDataCache();
        }
        return subSequenceCodes.containsKey(prefix);
    }

    private Action getActionFor(String code) {
        if (validCodes == null) {
            createDataCache();
        }
        return validCodes.get(code);
    }

    private Action getPossibleFutureSequentialActionFor(Action found, String foundUsing) {
        if (subSequenceCodes == null) {
            createDataCache();
        }

        int minimumSize = 1;
        if (found != null && found.getMatch() != Action.Match.any) {
            minimumSize = found.getCodes().size() + 1;
        }

        if (mDetectionHistory.size() == 0) {
            return found;
        }

        // if a single marker triggered found Action and it's not the last one in history then do
        // not provide a possible future sequential action as this will look confusing in the interface
        if (found != null && found.getMatch() == Action.Match.any && foundUsing != null) {
            MarkerDetectionRecord last = mDetectionHistory.get(mDetectionHistory.size() - 1);
            if (!foundUsing.equals(last.code)) {
                return found;
            }
        }

        if (found == null || found.getMatch() != Action.Match.all) {
            List<String> detectionHistoryAsStrings = new ArrayList<>();
            for (MarkerDetectionRecord record : mDetectionHistory) {
                detectionHistoryAsStrings.add(record.code);
            }
            for (int i = 0; i < detectionHistoryAsStrings.size(); ++i) {
                List<String> subHistory = detectionHistoryAsStrings.subList(i, detectionHistoryAsStrings.size());
                Set<Action> actions = subSequenceCodes.get(joinStr(subHistory, ">"));
                if (actions != null && !actions.isEmpty()) {
                    Action longestSequentialAction = null;
                    for (Action action : actions) {
                        if (action.getCodes().size() >= minimumSize && (longestSequentialAction == null
                                || longestSequentialAction.getCodes().size() < action.getCodes().size())) {
                            longestSequentialAction = action;
                        }
                    }
                    if (longestSequentialAction != null) {
                        return longestSequentialAction;
                    }
                }
            }
        }

        return found;
    }

    private Action getPossibleFutureGroupActionFor(Action found) {
        if (subGroupCodes == null) {
            createDataCache();
        }

        if (found == null || found.getMatch() != Action.Match.sequence) {

            List<String> detectedInFound = null;
            if (found != null) {
                detectedInFound = intersection(found.getCodes(), mCodesDetected);
            }

            Set<Action> groupFutureActions = subGroupCodes.get(joinStr(mCodesDetected, "+"));
            if (groupFutureActions != null && !groupFutureActions.isEmpty()) {
                Action largestGroupAction = null;
                for (Action action : groupFutureActions) {
                    if ((found == null || action.getCodes().containsAll(detectedInFound))
                            && (largestGroupAction == null
                                    || largestGroupAction.getCodes().size() < action.getCodes().size())) {
                        largestGroupAction = action;
                    }
                }
                if (largestGroupAction != null) {
                    return largestGroupAction;
                }
            }
        }

        return found;
    }

    private static List<String> intersection(List<String> list1, List<String> list2) {
        List<String> intersection = new ArrayList<>();
        if (list1 != null && list2 != null) {
            intersection.addAll(list1);
            intersection.retainAll(list2);
        }
        return intersection;
    }

    private void createDataCache() {
        if (validCodes == null) {
            validCodes = new HashMap<>();
            subGroupCodes = new HashMap<>();
            subSequenceCodes = new HashMap<>();
            for (Action action : experience.getActions()) {
                if (action.getMatch() == Action.Match.any || action.getCodes().size() == 1) // single
                {
                    for (String code : action.getCodes()) {
                        validCodes.put(code, action);
                    }
                } else if (action.getMatch() == Action.Match.all) // group
                {
                    String code = joinStr(action.getCodes(), "+");
                    validCodes.put(code, action);

                    List<Set<List<String>>> subGroupsByLength = new ArrayList<>();
                    combinationsOfStrings(action.getCodes(), action.getCodes().size() - 1, subGroupsByLength);
                    for (Set<List<String>> setOfGroups : subGroupsByLength) {
                        for (List<String> group : setOfGroups) {
                            code = joinStr(group, "+");
                            Set<Action> actions = subGroupCodes.get(code);
                            if (actions != null) {
                                actions.add(action);
                            } else {
                                HashSet<Action> actionsForSubGroup = new HashSet<>();
                                actionsForSubGroup.add(action);
                                subGroupCodes.put(code, actionsForSubGroup);
                            }
                        }
                    }
                } else if (action.getMatch() == Action.Match.sequence) {
                    String code = joinStr(action.getCodes(), ">");
                    validCodes.put(code, action);
                    for (int subCodeSize = 1; subCodeSize < action.getCodes().size(); ++subCodeSize) {
                        code = joinStr(action.getCodes().subList(0, subCodeSize), ">");
                        Set<Action> actions = subSequenceCodes.get(code);
                        if (actions != null) {
                            actions.add(action);
                        } else {
                            HashSet<Action> actionsForSubSequence = new HashSet<>();
                            actionsForSubSequence.add(action);
                            subSequenceCodes.put(code, actionsForSubSequence);
                        }
                    }
                }
            }
        }
    }
}