/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox 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 any later version. Jukefox 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 * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.jukefox.data.cache; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import ch.ethz.dcg.jukefox.commons.Constants; import ch.ethz.dcg.jukefox.commons.DataUnavailableException; import ch.ethz.dcg.jukefox.commons.DataWriteException; import ch.ethz.dcg.jukefox.commons.utils.JoinableThread; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.commons.utils.Pair; import ch.ethz.dcg.jukefox.commons.utils.kdtree.AdvancedKdTree; import ch.ethz.dcg.jukefox.data.db.IDbDataPortal; import ch.ethz.dcg.jukefox.manager.DirectoryManager; import ch.ethz.dcg.jukefox.manager.ModelSettingsManager; import ch.ethz.dcg.jukefox.manager.ResourceLoaderManager; import ch.ethz.dcg.jukefox.model.collection.BaseAlbum; import ch.ethz.dcg.jukefox.model.collection.CompleteTag; import ch.ethz.dcg.jukefox.model.collection.ListAlbum; import ch.ethz.dcg.jukefox.model.collection.MapAlbum; import ch.ethz.dcg.jukefox.model.libraryimport.ImportState; import edu.wlu.cs.levy.CG.KeySizeException; public class PreloadedDataManager implements MapDataCalculatorListener, ImportStateListener { private final static String TAG = PreloadedDataManager.class.getSimpleName(); public enum State { IDLE, LOADING, COMPUTING } private final IDbDataPortal dbDataPortal; private final ImportState importState; private final LinkedList<PreloadedDataManagerListener> listeners; private PreloadedData data; private PreloadedAlbums preloadedAlbums; private ResourceLoaderManager resourceLoaderManager; private final ModelSettingsManager modelSettingsManager; private JoinableThread tagLoaderThread; private JoinableThread songCoordsLoaderThread; private State state; private boolean aborted; private JoinableThread mapAlbumWriterThread; private JoinableThread songPcaCoordsWriterThread; private JoinableThread tagsWriterThread; /** * loader thread will set this exception if anything went wrong during load. */ private Exception loaderException; // TODO: is there a cleaner way to do private DirectoryManager directoryManager; // this? public PreloadedDataManager(IDbDataPortal dbDataPortal, ResourceLoaderManager resourceLoaderProvider, ImportState importState, ModelSettingsManager modelSettingsManager, DirectoryManager directoryManager) { this.dbDataPortal = dbDataPortal; this.resourceLoaderManager = resourceLoaderProvider; this.importState = importState; this.modelSettingsManager = modelSettingsManager; this.directoryManager = directoryManager; listeners = new LinkedList<PreloadedDataManagerListener>(); this.importState.addListener(this); } public void addListener(PreloadedDataManagerListener listener) { listeners.add(listener); } public synchronized void loadData() throws DataUnavailableException { setState(State.LOADING); Log.d(TAG, "load preloaded data called"); try { loaderException = null; PreloadedData data = new PreloadedData(); PreloadedAlbums preloadedAlbums = new PreloadedAlbums(dbDataPortal); loadAlbums(preloadedAlbums); // own thread loadTags(data); // own thread loadSongCoordsAndPcaCoords(data); // own thread // throws loader exception if something went wrong during load joinLoaderThreads(); if (aborted) { return; } // loadCacheFileData(data); setData(data, preloadedAlbums); // this call is synchronized Log.v(TAG, "preloaded data loaded."); } catch (Exception e) { Log.w(TAG, e); Log.v(TAG, "creating preloaded albums class"); // Load albums to ensure a quick loading of the activity PreloadedAlbums preloadedAlbums = new PreloadedAlbums(dbDataPortal); Log.v(TAG, "loading preloaded albums"); preloadedAlbums.loadFromDb(false); Log.v(TAG, "preloaded albums loaded."); this.preloadedAlbums = preloadedAlbums; modelSettingsManager.incRecomputeTaskId(); importState.setMapDataCalculated(false); importState.setMapDataCommitted(false); Log.v(TAG, "start recompute due to loader exception."); recomputeAsync(modelSettingsManager.getRecomputeTaskId()); } } private void recomputeAsync(final int recomputeTaskId) { importState.setImporting(true); Thread t = new Thread(new Runnable() { @Override public void run() { try { recompute(recomputeTaskId); } catch (DataUnavailableException e) { Log.w(TAG, e); } finally { importState.setImporting(false); } } }); t.start(); } private void joinLoaderThreads() throws Exception { try { songCoordsLoaderThread.realJoin(); tagLoaderThread.realJoin(); if (loaderException != null) { throw loaderException; } } catch (InterruptedException e) { Log.w(TAG, e); } } private void loadSongCoordsAndPcaCoords(final PreloadedData data) throws Exception { songCoordsLoaderThread = new JoinableThread(new Runnable() { @Override public void run() { try { List<PreloadedSongInfo> songInfos = dbDataPortal.getPreloadedSongInfo(); processSongInfos(songInfos, data); } catch (Exception e) { Log.w(TAG, "setting loader exception"); Log.w(TAG, e); loaderException = e; } } }); songCoordsLoaderThread.start(); } private void processSongInfos(List<PreloadedSongInfo> songInfos, PreloadedData data) { ArrayList<Integer> idsWithoutCoords = new ArrayList<Integer>(); ArrayList<Integer> idsWithCoords = new ArrayList<Integer>(); AdvancedKdTree<Integer> songKdTree = new AdvancedKdTree<Integer>(Constants.DIM); HashMap<Integer, float[]> pcaCoords = new HashMap<Integer, float[]>(); // int[] ids = new int[songInfos.size()]; // int i = 0; for (PreloadedSongInfo songInfo : songInfos) { if (songInfo.getSongPcaCoords() == null && songInfo.getSongCoords() != null) { throw new IllegalStateException("coords without pca coords, song id: " + songInfo.getSongId()); } if (songInfo.getSongPcaCoords() != null && songInfo.getSongCoords() == null) { throw new IllegalStateException("pca coords without coords, song id: " + songInfo.getSongId()); } if (songInfo.getSongCoords() == null) { idsWithoutCoords.add(songInfo.getSongId()); continue; } idsWithCoords.add(songInfo.getSongId()); try { songKdTree.insert(songInfo.getSongCoords(), songInfo.getSongId()); } catch (KeySizeException e) { Log.w(TAG, e); } pcaCoords.put(songInfo.getSongId(), songInfo.getSongPcaCoords()); // ids[i] = songInfo.getSongId(); // i++; } data.setPcaCoords(pcaCoords); data.setSongCoords(songKdTree, idsWithCoords, idsWithoutCoords); } private void loadTags(final PreloadedData data) { tagLoaderThread = new JoinableThread(new Runnable() { @Override public void run() { try { data.setTags(dbDataPortal.getCompleteTags(true)); } catch (Exception e) { Log.w(TAG, "setting loader exception"); Log.w(TAG, e); loaderException = e; } } }); tagLoaderThread.start(); } public void reloadAlbums() { PreloadedAlbums preloadedAlbums = new PreloadedAlbums(dbDataPortal); loadAlbums(preloadedAlbums); this.preloadedAlbums = preloadedAlbums; } public void recompute(int recomputeTaskId) throws DataUnavailableException { setState(State.COMPUTING); Log.v(TAG, "calculating map data..."); MapDataCalculator mapDataCalculator = new MapDataCalculator(dbDataPortal, importState, directoryManager); mapDataCalculator.addListener(this); Collection<ListAlbum> allAlbums = dbDataPortal.getAllAlbumsAsListAlbums(); List<CompleteTag> tags = getTags(); if (tags == null) { Log.w(TAG, "tags is null"); } mapDataCalculator.calculate(allAlbums, tags, preloadedAlbums, recomputeTaskId); importState.setMapDataCalculated(true); Log.v(TAG, "map data calculated."); joinDbWriterThreads(); Log.v(TAG, "map data written to db"); int currentRecomputeTaskId = modelSettingsManager.getRecomputeTaskId(); if (currentRecomputeTaskId == recomputeTaskId) { // set task id to 0, to mark that no further recompute is // required. modelSettingsManager.resetRecomputeTaskId(); } PreloadedAlbums preloadedAlbums = new PreloadedAlbums(dbDataPortal); Log.v(TAG, "loading albums from db"); preloadedAlbums.loadFromDb(true); PreloadedData data = new PreloadedData(); data.setSongCoords(mapDataCalculator.getSongCoords()); data.setIdsOfSongsWithoutCoords(mapDataCalculator.getIdsOfSongsWithoutCoords()); // TODO: Deal with songs without coordinates List<CompleteTag> relevantTags = mapDataCalculator.getRelevantTags(); data.setTags(relevantTags); data.setPcaCoords(mapDataCalculator.getSongPcaCoords()); // data.setAlbumQuadTree(mapDataCalculator.getAlbumQuadTree()); setData(data, preloadedAlbums); importState.setMapDataCommitted(true); } private List<CompleteTag> getTags() throws DataUnavailableException { List<CompleteTag> tags; try { tags = new ArrayList<CompleteTag>(dbDataPortal.getCompleteTags(false).values()); } catch (DataUnavailableException e) { Log.e(TAG, "getTags: Failed because of a database read exception"); throw e; } if (tags.size() < 500) { // something is wrong with our tag table... tags = resourceLoaderManager.readTags(); try { dbDataPortal.deleteTagTable(); } catch (DataWriteException e1) { Log.w(TAG, e1); throw new DataUnavailableException(e1); } dbDataPortal.batchInsertTags(tags); // int tagId; // // for (CompleteTag tag : tags) { // try { // tagId = dbDataPortal.insertTag(tag.getMeId(), tag.getName(), tag.getPlsaCoords()); // } catch (DataWriteException e) { // Log.e(TAG, "getTags: Failed because of a database write/insert exception"); // return null; // } // tag.setId(tagId); // } } return tags; } private void loadAlbums(PreloadedAlbums preloadedAlbums) { preloadedAlbums.loadFromDb(true); } private void setState(State state) { this.state = state; } public State getState() { return state; } public void abort() { this.aborted = true; } public synchronized PreloadedData getData() throws DataUnavailableException { if (data == null) { throw new DataUnavailableException("preloaded data not yet loaded"); } return data; } private synchronized void setData(PreloadedData data, PreloadedAlbums preloadedAlbums) { this.data = data; this.preloadedAlbums = preloadedAlbums; setState(State.IDLE); } public synchronized List<ListAlbum> getAllListAlbums() throws DataUnavailableException { if (preloadedAlbums == null) { waitForPreloadedAlbums(); } if (!preloadedAlbums.isLoaded()) { preloadedAlbums.loadFromDb(true); } return preloadedAlbums.getAllListAlbums(); } private void waitForPreloadedAlbums() throws DataUnavailableException { while (preloadedAlbums == null) { try { JoinableThread.sleep(20); } catch (InterruptedException e) { Log.w(TAG, e); } } } public synchronized Collection<MapAlbum> getAllMapAlbums() throws DataUnavailableException { if (preloadedAlbums == null) { Log.v(TAG, "preloaded albums == null => waiting for preloaded albums"); waitForPreloadedAlbums(); } if (!preloadedAlbums.isLoaded()) { Log.v(TAG, "loading preloaded albums"); preloadedAlbums.loadFromDb(true); } Log.v(TAG, "returning map albums from preloaded albums"); return preloadedAlbums.getAllMapAlbums(); } @Override public void onMapAlbumsCalculated(final Collection<MapAlbum> mapAlbums) { mapAlbumWriterThread = new JoinableThread(new Runnable() { @Override public void run() { int i = 0; // TODO: debug only... for (MapAlbum ma : mapAlbums) { i++; if (ma.getGridCoords() == null) { Log.v(TAG, "grid coords of album " + i + " are null"); } else { // Log.v(TAG, "album " + i + " has grid coords: [" // + ma.getGridCoords()[0] + ", " // + ma.getGridCoords()[1] + "]"); } } try { dbDataPortal.updateMapAlbumsPcaCoords(mapAlbums); } catch (DataWriteException e) { Log.e(TAG, "onMapAlbumsCalculated: update album pca coords failed!"); return; } } }); mapAlbumWriterThread.start(); } @Override public void onRelevantTagsCalculated(final Collection<CompleteTag> relevantTags) { tagsWriterThread = new JoinableThread(new Runnable() { @Override public void run() { try { dbDataPortal.setRelevantTags(relevantTags); } catch (DataWriteException e) { Log.e(TAG, "onRelevantTagsCalculated: tags insertion failed!"); return; } Log.v(TAG, "onRelevantTagsCalculated: tags inserted."); } }); tagsWriterThread.start(); } @Override public void onSongPcaCoordsCalculated(final HashMap<Integer, float[]> songPcaCoords) { songPcaCoordsWriterThread = new JoinableThread(new Runnable() { @Override public void run() { try { dbDataPortal.updateSongsPcaCoords(songPcaCoords); } catch (DataWriteException e) { Log.e(TAG, "onSongPcaCoordsCalculated: update song pca coords failed!"); return; } } }); songPcaCoordsWriterThread.start(); } private void joinDbWriterThreads() { try { // Log.v(TAG, "before join: mapAlbumWriterThread.state: " + // mapAlbumWriterThread.getState().name()); mapAlbumWriterThread.realJoin(); // Log.v(TAG, "after join: mapAlbumWriterThread.state: " + // mapAlbumWriterThread.getState().name()); mapAlbumWriterThread = null; Log.v(TAG, "mapAlbumWriterThread joined."); tagsWriterThread.realJoin(); Log.v(TAG, "tagsWriterThread joined."); songPcaCoordsWriterThread.realJoin(); Log.v(TAG, "songPcaCoordsWriterThread joined."); } catch (InterruptedException e) { Log.wtf(TAG, e); } } public synchronized List<Pair<MapAlbum, Float>> getSimilarAlbums(BaseAlbum album, int number) throws DataUnavailableException { if (preloadedAlbums == null) { Log.v(TAG, "preloaded albums == null => waiting for preloaded albums"); waitForPreloadedAlbums(); } if (!preloadedAlbums.isLoaded()) { Log.v(TAG, "loading preloaded albums"); preloadedAlbums.loadFromDb(true); } return preloadedAlbums.getSimilarAlbums(album, number); } @Override public void onAlbumCoversFetched() { Log.v(TAG, "album covers fetched."); Log.v(TAG, "reloading preloaded albums."); reloadAlbums(); } @Override public void onBaseDataCommitted() { reloadAlbums(); } @Override public void onCoordinatesFetched() { JoinableThread t = new JoinableThread(new Runnable() { @Override public void run() { int recomputeTaskId = modelSettingsManager.getRecomputeTaskId(); Log.v(TAG, "Recompute Map data id: " + recomputeTaskId); if (recomputeTaskId == 0) { importState.setMapDataCalculated(true); importState.setMapDataCommitted(true); return; } reloadAlbums(); abort(); try { recompute(recomputeTaskId); } catch (Exception e) { Log.w(TAG, e); // make sure the import get set to completed. importState.setMapDataCalculated(true); importState.setMapDataCommitted(true); } } }); t.start(); } @Override public void onImportAborted(boolean hadChanges) { } @Override public void onImportCompleted(boolean hadChanges) { } @Override public void onImportStarted() { } @Override public void onImportProblem(Throwable e) { } }