/* * opsu! - an open-source osu! client * Copyright (C) 2014 Jeffrey Han * * opsu! is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * opsu! 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with opsu!. If not, see . */ package itdelatrisu.opsu; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; /** * Indexed, expanding, doubly-linked list data type for song groups. */ public class OsuGroupList { /** * Sorting orders. */ public static final byte SORT_TITLE = 0, SORT_ARTIST = 1, SORT_CREATOR = 2, SORT_BPM = 3, SORT_MAX = 4; // not a sort /** * Sorting order names (indexed by SORT_* constants). */ public static final String[] SORT_NAMES = { "Title", "Artist", "Creator", "BPM" }; /** * List containing all parsed nodes. */ private ArrayList parsedNodes; /** * Total number of maps (i.e. OsuFile objects). */ private int mapCount = 0; /** * Song tags. * Each tag value is a HashSet which points song group ArrayLists. */ private HashMap>> tags; /** * Current list of nodes. * (For searches; otherwise, a pointer to parsedNodes.) */ private ArrayList nodes; /** * Index of current expanded node. * If no node is expanded, the value will be -1. */ private int expandedIndex = -1; /** * The last search query. */ private String lastQuery = ""; /** * Constructor. */ public OsuGroupList() { parsedNodes = new ArrayList(); nodes = parsedNodes; tags = new HashMap>>(); } /** * Returns the number of elements. */ public int size() { return nodes.size(); } /** * Adds a song group. * @param osuFiles the list of OsuFile objects in the group */ public void addSongGroup(ArrayList osuFiles) { parsedNodes.add(new OsuGroupNode(osuFiles)); mapCount += osuFiles.size(); } /** * Returns the total number of parsed maps (i.e. OsuFile objects). */ public int getMapCount() { return mapCount; } /** * Adds a tag. * @param tag the tag string (key) * @param osuFiles the song group associated with the tag (value) */ public void addTag(String tag, ArrayList osuFiles) { tag = tag.toLowerCase(); HashSet> tagSet = tags.get(tag); if (tagSet == null) { tagSet = new HashSet>(); tags.put(tag, tagSet); } tagSet.add(osuFiles); } /** * Returns the OsuGroupNode at an index, disregarding expansions. */ public OsuGroupNode getBaseNode(int index) { if (index < 0 || index >= size()) return null; return nodes.get(index); } /** * Returns a random base node. */ public OsuGroupNode getRandomNode() { return getBaseNode((int) (Math.random() * size())); } /** * Returns the OsuGroupNode a given number of positions forward or backwards. * @param node the starting node * @param shift the number of nodes to shift forward (+) or backward (-). */ public OsuGroupNode getNode(OsuGroupNode node, int shift) { OsuGroupNode startNode = node; if (shift > 0) { for (int i = 0; i < shift && startNode != null; i++) startNode = startNode.next; } else { for (int i = 0; i < shift && startNode != null; i++) startNode = startNode.prev; } return startNode; } /** * Returns the index of the expanded node (or -1 if nothing is expanded). */ public int getExpandedIndex() { return expandedIndex; } /** * Expands the node at an index by inserting a new node for each OsuFile in that node. */ public void expand(int index) { // undo the previous expansion if (unexpand() == index) return; // don't re-expand the same node OsuGroupNode node = getBaseNode(index); if (node == null) return; ArrayList osuFiles = node.osuFiles; OsuGroupNode nextNode = node.next; for (int i = 0; i < node.osuFiles.size(); i++) { OsuGroupNode newNode = new OsuGroupNode(osuFiles); newNode.index = index; newNode.osuFileIndex = i; newNode.prev = node; node.next = newNode; node = node.next; } if (nextNode != null) { node.next = nextNode; nextNode.prev = node; } expandedIndex = index; } /** * Undoes the current expansion, if any. * @return the index of the previously expanded node, or -1 if no expansions */ private int unexpand() { if (expandedIndex < 0 || expandedIndex >= size()) return -1; // reset [base].next and [base+1].prev OsuGroupNode eCur = getBaseNode(expandedIndex); OsuGroupNode eNext = getBaseNode(expandedIndex + 1); eCur.next = eNext; if (eNext != null) eNext.prev = eCur; int oldIndex = expandedIndex; expandedIndex = -1; return oldIndex; } /** * Initializes the links in the list, given a sorting order (SORT_* constants). */ public void init(byte order) { if (size() < 1) return; // sort the list switch (order) { case SORT_TITLE: Collections.sort(nodes); break; case SORT_ARTIST: Collections.sort(nodes, new OsuGroupNode.ArtistOrder()); break; case SORT_CREATOR: Collections.sort(nodes, new OsuGroupNode.CreatorOrder()); break; case SORT_BPM: Collections.sort(nodes, new OsuGroupNode.BPMOrder()); break; } expandedIndex = -1; // create links OsuGroupNode lastNode = nodes.get(0); lastNode.index = 0; lastNode.prev = null; for (int i = 1, size = size(); i < size; i++) { OsuGroupNode node = nodes.get(i); lastNode.next = node; node.index = i; node.prev = lastNode; lastNode = node; } lastNode.next = null; } /** * Creates a new list of song groups in which each group contains a match to a search query. * @param query the search query (tag terms separated by spaces) * @return false if query is the same as the previous one, true otherwise */ public boolean search(String query) { if (query == null) return false; // don't redo the same search query = query.trim().toLowerCase(); if (lastQuery != null && query.equals(lastQuery)) return false; lastQuery = query; // if empty query, reset to original list if (query.isEmpty()) { nodes = parsedNodes; return true; } // tag search: check if each word is contained in global tag structure HashSet> taggedGroups = new HashSet>(); String[] terms = query.split("\\s+"); for (String term : terms) { if (tags.containsKey(term)) taggedGroups.addAll(tags.get(term)); // add all matches } // traverse parsedNodes, comparing each element with the query nodes = new ArrayList(); for (OsuGroupNode node : parsedNodes) { // search: tags if (taggedGroups.contains(node.osuFiles)) { nodes.add(node); continue; } OsuFile osu = node.osuFiles.get(0); // search: title, artist, creator, source, version (first OsuFile) if (osu.title.toLowerCase().contains(query) || osu.artist.toLowerCase().contains(query) || osu.creator.toLowerCase().contains(query) || osu.source.toLowerCase().contains(query) || osu.version.toLowerCase().contains(query)) { nodes.add(node); continue; } // search: versions (all OsuFiles) for (int i = 1; i < node.osuFiles.size(); i++) { if (node.osuFiles.get(i).version.toLowerCase().contains(query)) { nodes.add(node); break; } } } return true; } }