package de.westnordost.streetcomplete.data.osmnotes; import android.content.SharedPreferences; import android.graphics.Rect; import android.util.Log; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import javax.inject.Inject; import de.westnordost.streetcomplete.ApplicationConstants; import de.westnordost.streetcomplete.data.QuestGroup; import de.westnordost.streetcomplete.data.QuestStatus; import de.westnordost.streetcomplete.Prefs; import de.westnordost.streetcomplete.data.VisibleQuestListener; import de.westnordost.streetcomplete.data.tiles.DownloadedTilesDao; import de.westnordost.streetcomplete.util.SlippyMapMath; import de.westnordost.osmapi.common.Handler; import de.westnordost.osmapi.map.data.BoundingBox; import de.westnordost.osmapi.map.data.LatLon; import de.westnordost.osmapi.notes.Note; import de.westnordost.osmapi.notes.NoteComment; import de.westnordost.osmapi.notes.NotesDao; public class OsmNotesDownload { private static final String TAG = "QuestDownload"; private final NotesDao noteServer; private final NoteDao noteDB; private final OsmNoteQuestDao noteQuestDB; private final CreateNoteDao createNoteDB; private final DownloadedTilesDao downloadedTilesDao; private final SharedPreferences preferences; private VisibleQuestListener listener; @Inject public OsmNotesDownload( NotesDao noteServer, NoteDao noteDB, OsmNoteQuestDao noteQuestDB, CreateNoteDao createNoteDB, DownloadedTilesDao downloadedTilesDao, SharedPreferences preferences) { this.noteServer = noteServer; this.noteDB = noteDB; this.noteQuestDB = noteQuestDB; this.createNoteDB = createNoteDB; this.downloadedTilesDao = downloadedTilesDao; this.preferences = preferences; } public void setQuestListener(VisibleQuestListener listener) { this.listener = listener; } public Set<LatLon> download(final Rect tiles, final Long userId, int max) { BoundingBox bbox = SlippyMapMath.asBoundingBox(tiles, ApplicationConstants.QUEST_TILE_ZOOM); final Set<LatLon> positions = new HashSet<>(); final HashMap<Long, Long> previousQuestsByNoteId = getPreviousQuestsByNoteId(bbox); final Collection<Note> notes = new ArrayList<>(); final Collection<OsmNoteQuest> quests = new ArrayList<>(); final Collection<OsmNoteQuest> hiddenQuests = new ArrayList<>(); noteServer.getAll(bbox, new Handler<Note>() { @Override public void handle(Note note) { OsmNoteQuest quest = new OsmNoteQuest(note); if(makeNoteHidden(userId, note)) { quest.setStatus(QuestStatus.HIDDEN); hiddenQuests.add(quest); } else if(makeNoteInvisible(quest)) { quest.setStatus(QuestStatus.INVISIBLE); hiddenQuests.add(quest); } else { quests.add(quest); previousQuestsByNoteId.remove(note.id); } notes.add(note); positions.add(note.position); } }, max, 0); noteDB.putAll(notes); int hiddenAmount = noteQuestDB.replaceAll(hiddenQuests); int newAmount = noteQuestDB.addAll(quests); int visibleAmount = quests.size(); if(listener != null) { Iterator<OsmNoteQuest> it = quests.iterator(); while(it.hasNext()) { // it is null if this quest is already in the DB, so don't call onQuestCreated if(it.next().getId() == null) it.remove(); } listener.onQuestsCreated(quests, QuestGroup.OSM_NOTE); /* we do not call listener.onNoteQuestRemoved for hiddenQuests here, because on * replacing hiddenQuests into DB, they get new quest IDs. As far as the DB is concerned, * hidden note quests are always new quests which are hidden. * If a note quest was visible before, it'll be removed below when the previous quests * are cleared */ } /* delete note quests created in a previous run in the given bounding box that are not found again -> these notes have been closed/solved/removed */ if(previousQuestsByNoteId.size() > 0) { if(listener != null) { listener.onQuestsRemoved(previousQuestsByNoteId.values(), QuestGroup.OSM_NOTE); } noteQuestDB.deleteAll(previousQuestsByNoteId.values()); noteDB.deleteUnreferenced(); } for(CreateNote createNote : createNoteDB.getAll(bbox)) { positions.add(createNote.position); } int closedAmount = previousQuestsByNoteId.size(); downloadedTilesDao.putQuestType(tiles, OsmNoteQuest.type.getClass().getSimpleName()); Log.i(TAG, "Successfully added " + newAmount + " new and removed " + closedAmount + " closed notes (" + hiddenAmount + " of " + (hiddenAmount + visibleAmount) + " notes are hidden)"); return positions; } private HashMap<Long, Long> getPreviousQuestsByNoteId(BoundingBox bbox) { HashMap<Long, Long> result = new HashMap<>(); for(OsmNoteQuest quest : noteQuestDB.getAll(bbox, null)) { result.put(quest.getNote().id, quest.getId()); } return result; } private boolean makeNoteHidden(Long userId, Note note) { /* hide a note if he already contributed to it. This can also happen from outside this application, which is why we need to overwrite its quest status. */ return containsCommentFromUser(userId, note); } // the difference to hidden is that is that invisible quests may turn visible again, dependent // on the user's settings while hidden quests are "dead" private boolean makeNoteInvisible(OsmNoteQuest quest) { /* many notes are created to report problems on the map that cannot be resolved * through an on-site survey rather than questions from other (armchair) mappers * that want something cleared up on-site. * Likely, if something is posed as a question, the reporter expects someone to * answer/comment on it, so let's only show these */ boolean showNonQuestionNotes = preferences.getBoolean(Prefs.SHOW_NOTES_NOT_PHRASED_AS_QUESTIONS, false); return !(quest.probablyContainsQuestion() || showNonQuestionNotes); } private boolean containsCommentFromUser(Long userId, Note note) { if(userId == null) return false; for(NoteComment comment : note.comments) { if(comment.user != null && comment.user.id == userId) return true; } return false; } }