package de.westnordost.streetcomplete.data.osm.download;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import de.westnordost.streetcomplete.ApplicationConstants;
import de.westnordost.streetcomplete.data.QuestGroup;
import de.westnordost.streetcomplete.data.QuestType;
import de.westnordost.streetcomplete.data.VisibleQuestListener;
import de.westnordost.streetcomplete.data.osm.ElementGeometry;
import de.westnordost.streetcomplete.data.osm.OsmElementQuestType;
import de.westnordost.streetcomplete.data.osm.OsmQuest;
import de.westnordost.streetcomplete.data.osm.persist.ElementGeometryDao;
import de.westnordost.streetcomplete.data.osm.persist.MergedElementDao;
import de.westnordost.streetcomplete.data.osm.persist.OsmQuestDao;
import de.westnordost.streetcomplete.data.osm.persist.OsmElementKey;
import de.westnordost.streetcomplete.data.tiles.DownloadedTilesDao;
import de.westnordost.streetcomplete.util.SlippyMapMath;
import de.westnordost.osmapi.map.data.BoundingBox;
import de.westnordost.osmapi.map.data.Element;
import de.westnordost.osmapi.map.data.LatLon;
// TODO test case
public class OsmQuestDownload
{
private static final String TAG = "QuestDownload";
// injections
private final ElementGeometryDao geometryDB;
private final MergedElementDao elementDB;
private final OsmQuestDao osmQuestDB;
private final DownloadedTilesDao downloadedTilesDao;
// listener
private VisibleQuestListener questListener;
@Inject public OsmQuestDownload(
ElementGeometryDao geometryDB,
MergedElementDao elementDB, OsmQuestDao osmQuestDB,
DownloadedTilesDao downloadedTilesDao)
{
this.geometryDB = geometryDB;
this.elementDB = elementDB;
this.osmQuestDB = osmQuestDB;
this.downloadedTilesDao = downloadedTilesDao;
}
public void setQuestListener(VisibleQuestListener listener)
{
this.questListener = listener;
}
public int download(final OsmElementQuestType questType, Rect tiles,
final Set<LatLon> blacklistedPositions)
{
BoundingBox bbox = SlippyMapMath.asBoundingBox(tiles, ApplicationConstants.QUEST_TILE_ZOOM);
final ArrayList<ElementGeometryDao.Row> geometryRows = new ArrayList<>();
final Map<OsmElementKey,Element> elements = new HashMap<>();
final ArrayList<OsmQuest> quests = new ArrayList<>();
final Map<OsmElementKey, Long> previousQuests = getPreviousQuestsIdsByElementKey(questType, bbox);
boolean success = questType.download(bbox, new MapDataWithGeometryHandler()
{
@Override public void handle(@NonNull Element element, @Nullable ElementGeometry geometry)
{
if(mayCreateQuestFrom(questType, element, geometry, blacklistedPositions))
{
Element.Type elementType = element.getType();
long elementId = element.getId();
OsmQuest quest = new OsmQuest(questType, elementType, elementId, geometry);
geometryRows.add(new ElementGeometryDao.Row(
elementType, elementId, quest.getGeometry()));
quests.add(quest);
OsmElementKey elementKey = new OsmElementKey(elementType, elementId);
elements.put(elementKey, element);
previousQuests.remove(elementKey);
}
}
});
if(!success) return 0;
// geometry and elements must be put into DB first because quests have foreign keys on it
geometryDB.putAll(geometryRows);
elementDB.putAll(elements.values());
int newQuestsByQuestType = osmQuestDB.addAll(quests);
if(questListener != null && !quests.isEmpty())
{
Iterator<OsmQuest> 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();
}
questListener.onQuestsCreated(quests, QuestGroup.OSM);
}
if(!previousQuests.isEmpty())
{
if(questListener != null)
{
questListener.onQuestsRemoved(previousQuests.values(), QuestGroup.OSM);
}
osmQuestDB.deleteAll(previousQuests.values());
}
// note: this could be done after ALL osm quest types have been downloaded if this
// turns out to be slow if done for every quest type
geometryDB.deleteUnreferenced();
elementDB.deleteUnreferenced();
downloadedTilesDao.putQuestType(tiles, getQuestTypeName(questType));
int visibleQuestsByQuestType = quests.size();
int obsoleteAmount = previousQuests.size();
Log.i(TAG, getQuestTypeName(questType) + ": " +
"Added " + newQuestsByQuestType + " new and " +
"removed " + obsoleteAmount + " already resolved quests." +
" (Total: " + visibleQuestsByQuestType + ")");
return visibleQuestsByQuestType;
}
private Map<OsmElementKey, Long> getPreviousQuestsIdsByElementKey(
OsmElementQuestType questType, BoundingBox bbox)
{
String questTypeName = questType.getClass().getSimpleName();
Map<OsmElementKey, Long> result = new HashMap<>();
for(OsmQuest quest : osmQuestDB.getAll(bbox, null, questTypeName, null, null))
{
result.put(new OsmElementKey(quest.getElementType(), quest.getElementId()), quest.getId());
}
return result;
}
private boolean mayCreateQuestFrom(OsmElementQuestType questType, Element element,
ElementGeometry geometry, Set<LatLon> blacklistedPositions)
{
// invalid geometry -> can't show this quest, so skip it
if(geometry == null)
{
// classified as warning because it might very well be a bug on the geometry
// creation on our side
Log.w(TAG, getQuestTypeName(questType) + ": Not adding a quest " +
" because the element " + getElementAsLogString(element) +
" has no valid geometry");
return false;
}
// do not create quests whose marker is at a blacklisted position
if(blacklistedPositions != null && blacklistedPositions.contains(geometry.center))
{
Log.d(TAG, getQuestTypeName(questType) + ": Not adding a quest at " +
getPosAsLogString(geometry.center) +
" because there is a note at that position");
return false;
}
return true;
}
private static String getElementAsLogString(Element element)
{
return element.getType().name().toLowerCase() + " #" + element.getId();
}
private static String getQuestTypeName(QuestType q)
{
return q.getClass().getSimpleName();
}
private static String getPosAsLogString(LatLon pos)
{
return pos.getLatitude() + ", " + pos.getLongitude();
}
}