diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a8042302f0..23efd254a7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -176,6 +176,9 @@ dependencies { // opening hours parser implementation("ch.poole:OpeningHoursParser:0.25.0") + + // sunset-sunrise parser for lit quests + implementation("com.luckycatlabs:SunriseSunsetCalculator:1.2") } /** Localizations that should be pulled from POEditor etc. */ diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/quest/DayNightCycle.kt b/app/src/main/java/de/westnordost/streetcomplete/data/quest/DayNightCycle.kt new file mode 100644 index 0000000000..1e3b34fa3f --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/data/quest/DayNightCycle.kt @@ -0,0 +1,3 @@ +package de.westnordost.streetcomplete.data.quest + +enum class DayNightCycle { DAY_AND_NIGHT, ONLY_DAY, ONLY_NIGHT } diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestType.kt b/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestType.kt index 44381c6323..861c7c34ab 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestType.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestType.kt @@ -19,4 +19,7 @@ interface QuestType { /** The quest type can clean it's metadata that is older than the given timestamp here, if any */ fun deleteMetadataOlderThan(timestamp: Long) {} + + /** if the quest should only be shown during day-light os night-time hours */ + val dayNightCycle: DayNightCycle get() = DayNightCycle.DAY_AND_NIGHT } diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt b/app/src/main/java/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt index 879cd348e3..25a8ba8855 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt @@ -5,6 +5,7 @@ import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuest import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestSource import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuest import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestSource +import de.westnordost.streetcomplete.data.visiblequests.DayNightQuestFilter import de.westnordost.streetcomplete.data.visiblequests.TeamModeQuestFilter import de.westnordost.streetcomplete.data.visiblequests.VisibleQuestTypeSource import java.util.concurrent.CopyOnWriteArrayList @@ -17,7 +18,8 @@ import javax.inject.Singleton private val osmQuestSource: OsmQuestSource, private val osmNoteQuestSource: OsmNoteQuestSource, private val visibleQuestTypeSource: VisibleQuestTypeSource, - private val teamModeQuestFilter: TeamModeQuestFilter + private val teamModeQuestFilter: TeamModeQuestFilter, + private val dayNightQuestFilter: DayNightQuestFilter, ) { interface Listener { /** Called when given quests in the given group have been added/removed */ @@ -82,7 +84,7 @@ import javax.inject.Singleton } private fun isVisible(quest: Quest): Boolean = - visibleQuestTypeSource.isVisible(quest.type) && teamModeQuestFilter.isVisible(quest) + visibleQuestTypeSource.isVisible(quest.type) && teamModeQuestFilter.isVisible(quest) && dayNightQuestFilter.isVisible(quest) fun addListener(listener: Listener) { listeners.add(listener) diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/visiblequests/DayNightQuestFilter.kt b/app/src/main/java/de/westnordost/streetcomplete/data/visiblequests/DayNightQuestFilter.kt new file mode 100644 index 0000000000..a74c98220c --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/data/visiblequests/DayNightQuestFilter.kt @@ -0,0 +1,20 @@ +package de.westnordost.streetcomplete.data.visiblequests + +import de.westnordost.streetcomplete.data.quest.DayNightCycle.* +import de.westnordost.streetcomplete.data.quest.Quest +import de.westnordost.streetcomplete.util.isDay +import javax.inject.Inject + +class DayNightQuestFilter @Inject internal constructor() { + /* + Might be an idea to add a listener so this is reevaluated occasionally, or something like that. + However, I think it's reevaluated everytime the displayed quests are updated? + */ + fun isVisible(quest: Quest): Boolean { + return when (quest.type.dayNightCycle) { + DAY_AND_NIGHT -> true + ONLY_DAY -> isDay(quest.position) + ONLY_NIGHT -> !isDay(quest.position) + } + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestModule.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestModule.kt index 2caa60f150..f3239d4ac8 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestModule.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestModule.kt @@ -128,7 +128,11 @@ import javax.inject.Singleton // ↓ 1. notes OsmNoteQuestType, - // ↓ 2. important data that is used by many data consumers + // ↓ 2. time-specific quests, such as lit quests which are moved to only show ar night + AddWayLit(), // used by OsmAnd if "Street lighting" is enabled. (Configure map, Map rendering, Details) + AddBusStopLit(), + + // ↓ 3. important data that is used by many data consumers AddRoadName(), AddPlaceName(featureDictionaryFuture), AddOneway(), @@ -152,7 +156,7 @@ import javax.inject.Singleton AddReligionToPlaceOfWorship(), // icons on maps are different - OSM Carto, mapy.cz, OsmAnd, Sputnik etc AddParkingAccess(), //OSM Carto, mapy.cz, OSMand, Sputnik etc - // ↓ 3. useful data that is used by some data consumers + // ↓ 4. useful data that is used by some data consumers AddRecyclingType(), AddRecyclingContainerMaterials(), AddSport(), @@ -191,15 +195,14 @@ import javax.inject.Singleton AddFerryAccessMotorVehicle(), AddAcceptsCash(featureDictionaryFuture), - // ↓ 4. definitely shown as errors in QA tools + // ↓ 5. definitely shown as errors in QA tools - // ↓ 5. may be shown as missing in QA tools + // ↓ 6. may be shown as missing in QA tools DetermineRecyclingGlass(), // because most recycling:glass=yes is a tagging mistake - // ↓ 6. may be shown as possibly missing in QA tools + // ↓ 7. may be shown as possibly missing in QA tools - // ↓ 7. data useful for only a specific use case - AddWayLit(), // used by OsmAnd if "Street lighting" is enabled. (Configure map, Map rendering, Details) + // ↓ 8. data useful for only a specific use case AddToiletsFee(), // used by OsmAnd in the object description AddBabyChangingTable(), // used by OsmAnd in the object description AddBikeParkingCover(), // used by OsmAnd in the object description @@ -237,10 +240,10 @@ import javax.inject.Singleton AddBollardType(), // useful for first responders AddCameraType(), - // ↓ 8. defined in the wiki, but not really used by anyone yet. Just collected for + // ↓ 9. defined in the wiki, but not really used by anyone yet. Just collected for // the sake of mapping it in case it makes sense later AddPitchSurface(), - AddPitchLit(), + AddPitchLit(), // Not affected by new DayNight cycle because the lights are usually only on during games AddIsDefibrillatorIndoor(), AddSummitRegister(), AddCyclewayPartSurface(), @@ -256,7 +259,6 @@ import javax.inject.Singleton AddCarWashType(), AddBenchStatusOnBusStop(), AddBinStatusOnBusStop(), - AddBusStopLit(), AddBenchBackrest(), AddTrafficSignalsButton(), AddPostboxRoyalCypher() diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt index 128726e5a7..462a5b50be 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt @@ -4,6 +4,7 @@ import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.data.meta.updateWithCheckDate import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType +import de.westnordost.streetcomplete.data.quest.DayNightCycle.ONLY_NIGHT import de.westnordost.streetcomplete.ktx.arrayOfNotNull import de.westnordost.streetcomplete.ktx.containsAnyKey import de.westnordost.streetcomplete.ktx.toYesNo @@ -29,6 +30,7 @@ class AddBusStopLit : OsmFilterQuestType() { override val commitMessage = "Add whether a bus stop is lit" override val wikiLink = "Key:lit" override val icon = R.drawable.ic_quest_bus_stop_lit + override val dayNightCycle = ONLY_NIGHT override fun getTitle(tags: Map): Int { val hasName = tags.containsAnyKey("name", "ref") diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt index 69191036e6..ce79be6210 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt @@ -3,8 +3,9 @@ package de.westnordost.streetcomplete.quests.way_lit import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.data.meta.MAXSPEED_TYPE_KEYS import de.westnordost.streetcomplete.data.meta.updateWithCheckDate -import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder +import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType +import de.westnordost.streetcomplete.data.quest.DayNightCycle.ONLY_NIGHT class AddWayLit : OsmFilterQuestType() { @@ -44,6 +45,7 @@ class AddWayLit : OsmFilterQuestType() { override val wikiLink = "Key:lit" override val icon = R.drawable.ic_quest_lantern override val isSplitWayEnabled = true + override val dayNightCycle = ONLY_NIGHT override fun getTitle(tags: Map): Int { val type = tags["highway"] diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/CheckIfDay.kt b/app/src/main/java/de/westnordost/streetcomplete/util/CheckIfDay.kt new file mode 100644 index 0000000000..bd3cfe7928 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/util/CheckIfDay.kt @@ -0,0 +1,41 @@ +package de.westnordost.streetcomplete.util + +import com.luckycatlabs.sunrisesunset.Zenith +import com.luckycatlabs.sunrisesunset.calculator.SolarEventCalculator +import com.luckycatlabs.sunrisesunset.dto.Location +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.* + +fun localDateToCalendar(localDate: LocalDate): Calendar { + val calendar = Calendar.getInstance() + calendar.set(localDate.year, localDate.month.value, localDate.dayOfMonth) + return calendar +} + +fun isDay(pos: LatLon): Boolean { + /* This functions job is to check if it's currently light out. + It will use the location of the node and check the civil sunrise/sunset time. + + Sometimes sunset is after midnight. This would actually cause sunset to be before sunrise (as it's checking 00:00 to 23:59, it'll catch the day before!), so to gnt around this, we check the next day if that's the case. + */ + + val timezone = TimeZone.getDefault().id + val location = Location(pos.latitude, pos.longitude) + val calculator = SolarEventCalculator(location, timezone) + val now = ZonedDateTime.now(ZoneId.of(timezone)) + val today = now.toLocalDate() + + val sunrise = ZonedDateTime.of(today, LocalTime.parse(calculator.computeSunriseTime(Zenith.CIVIL, localDateToCalendar(today))), ZoneId.of(timezone)) + val sunset = ZonedDateTime.of(today, LocalTime.parse(calculator.computeSunsetTime(Zenith.CIVIL, localDateToCalendar(today))), ZoneId.of(timezone)) + return if (sunset < sunrise) { + + val sunset = ZonedDateTime.of(today.plusDays(1), LocalTime.parse(calculator.computeSunsetTime(Zenith.CIVIL, localDateToCalendar(today.plusDays(1)))), ZoneId.of(timezone)) + now in sunrise..sunset + } else { + now in sunrise..sunset + } +}