diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index a695f84..ba11eb4 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -2,7 +2,7 @@ {"id":"tilfluktsrom-5s7","title":"PWA: manuell testing mangler","description":"Mirror av Forgejo-issue #1.\n\nPWA-versjonen (pwa/) er skrevet, men ikke manuelt testet i nettleser. Enhetstestene passerer, men appen må verifiseres i praksis:\n\n- Start utviklingsserver og test i Chrome/Firefox\n- Test offline-modus (service worker)\n- Test kompass (iOS Safari + Android Chrome)\n- Test installasjon via «Legg til på startskjerm»\n- Test kartbufring og offline kartvisning\n- Test på fysisk iPhone (iOS-spesifikk kompasshåndtering)\n- Test i18n (norsk bokmål, nynorsk, engelsk)\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/1","status":"closed","priority":2,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:57:04Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:02:57Z","closed_at":"2026-04-29T14:02:57Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"tilfluktsrom-nt8","title":"Åpne i kartapp for gangvei til tilfluktsrom","description":"Mirror av Forgejo-issue #2.\n\nLegg til knapp (på bunnark og/eller kompassvisning) som åpner gangveibeskrivelse til valgt tilfluktsrom i ekstern kartapp.\n\nImplementasjon:\n- ACTION_VIEW intent med geo: URI: geo:lat,lon?q=lat,lon(Tilfluktsrom - adresse)\n- geo: håndteres av tilgjengelig kartapp (OsmAnd, Organic Maps, Google Maps, ...)\n- OsmAnd og Organic Maps støtter offline-navigasjon med geo: — ideelt for degradert nett\n- IKKE hardkode Google Maps-URL-er — bruk geo:\n- Faller pent tilbake hvis ingen kartapp er installert (Toast med koordinater å kopiere)\n- Knapp ved siden av tilfluktsrom-adresse i bunnarket\n\nI en akuttsituasjon er det å finne tilfluktsrommet på kartet bare halve problemet — du må vite gangveien dit. geo:-intent fungerer med offline-kapable kartapper, kritisk når nettet er nede.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/2","status":"open","priority":2,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:57:03Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:57:03Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"tilfluktsrom-gmu","title":"Test og ferdigstill PWA-versjonen","description":"Mirror av Forgejo-issue #7.\n\nFå den eksisterende PWA-en i pwa/-katalogen til å fungere og testet som webfallback.\n\nStatus:\n- Vite + TypeScript + Leaflet + idb + vite-plugin-pwa\n- Shelter-data forhåndsprosesseres ved bygg (scripts/fetch-shelters.ts)\n- Markert som ikke-testet i README (issue #1)\n\nOppgaver:\n- bun install + bun run dev — fikse byggefeil\n- Verifiser at bun run fetch-shelters genererer public/data/shelters.json\n- Test offline (service worker)\n- Test på mobilnettleser (iOS Safari, Android Chrome)\n- Deploy til statisk hosting\n- Lenke til PWA fra Android-appens om-side eller README\n\nWebfallback for iOS-brukere og folk uten Android-app. Også raskest tilgang i en akuttsituasjon.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/7","status":"closed","priority":2,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:46Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:02:57Z","closed_at":"2026-04-29T14:02:57Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"tilfluktsrom-9sf","title":"Dyplenket tilfluktsrom utenfor lista vises ikke","description":"Mirror av Forgejo-issue #13.\n\nNår en dyplenke åpner et tilfluktsrom som ikke er blant de 3 nærmeste, blir det valgt i kartet, men vises ikke i lista i bunnpanelet. Brukeren ser ikke hva som er valgt.\n\nForslag:\n1. Legg til det dyplenkede tilfluktsrommet som ekstra element i lista (med markering om at det ikke er blant de 3 nærmeste), eller\n2. Rull lista slik at det valgte elementet er synlig.\n\nIdentifisert i bruksanalyse. Moderat prioritet.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/13","status":"open","priority":2,"issue_type":"bug","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:15Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:15Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"tilfluktsrom-9sf","title":"Dyplenket tilfluktsrom utenfor lista vises ikke","description":"Mirror av Forgejo-issue #13.\n\nNår en dyplenke åpner et tilfluktsrom som ikke er blant de 3 nærmeste, blir det valgt i kartet, men vises ikke i lista i bunnpanelet. Brukeren ser ikke hva som er valgt.\n\nForslag:\n1. Legg til det dyplenkede tilfluktsrommet som ekstra element i lista (med markering om at det ikke er blant de 3 nærmeste), eller\n2. Rull lista slik at det valgte elementet er synlig.\n\nIdentifisert i bruksanalyse. Moderat prioritet.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/13","status":"closed","priority":2,"issue_type":"bug","assignee":"Ole-Morten Duesund","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:15Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T14:49:15Z","started_at":"2026-04-29T14:46:18Z","closed_at":"2026-04-29T14:49:15Z","close_reason":"Hybrid implementert: ShelterListItem-wrapper med isOutsideNearest-flagg, badge i item_shelter.xml, smoothScrollToPosition på rebuildShelterList. Visuell verifisering på enhet/emulator gjenstår.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"tilfluktsrom-jmv","title":"Geonorge: lokalId regenereres på hver eksport — bytt til romnr som ekstern nøkkel","description":"Mirror av Forgejo-issue #15.\n\nTilfluktsromdata fra Geonorge regenererer lokalId-feltet ved hver eksport (verifisert: alle 556 lokalId-er endres mellom snapshots, mens romnr/plasser/adresse/koordinater er stabile).\n\nKonsekvens: delingslenker basert på lokalId ble brutt mellom datasett-oppdateringer. Vi har allerede byttet ekstern delingsidentifikator til romnr og beholdt lokalId som intern Room-PK.\n\nGjenstår:\n- Spørre Geonorge/DSB hvorfor lokalId regenereres (tilsiktet gml:id-stil eller FME/SOSI-feil?)\n- Hvis feil: be om at lokalId persisteres mellom eksporter\n- Hvis tilsiktet: be om dokumentasjon\n- Sjekke om WFS-endepunktet returnerer stabile ID-er\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/15","status":"open","priority":2,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-29T13:55:59Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:55:59Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"tilfluktsrom-jvn","title":"Filtrere tilfluktsrom etter minimumskapasitet","description":"Mirror av Forgejo-issue #5.\n\nLegg til filter for minimumskapasitet slik at brukere kan finne tilfluktsrom store nok for gruppen sin.\n\nImplementasjon:\n- Filterchip eller dropdown over tilfluktsromlista (f.eks. \"Min. plasser: 50 / 100 / 200 / Alle\")\n- Filteret gjelder både nærmeste-lista og kartmarkørene\n- Lagre valg i SharedPreferences\n- Default: vis alle (intet filter)\n\nSkoler, arbeidsplasser og familier trenger tilfluktsrom med nok kapasitet. Et lite tilfluktsrom med 20 plasser er ubrukelig for en gruppe på 50. Enkel UX-forbedring med reell praktisk verdi.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/5","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:51Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"tilfluktsrom-nyz","title":"Støtte for internasjonale tilfluktsromdata","description":"Mirror av Forgejo-issue #9.\n\nI dag støtter Tilfluktsrom kun norske data fra Geonorge (GeoJSON, EPSG:25833). Mål: identifisere og integrere data fra andre land.\n\nMål:\n1. Identifisere internasjonale datakilder (NO, SE/MSB, FI/Pelastustoimi, CH/FOCP, SG/SCDF, US/FEMA)\n2. Støtte flere dataformater uten å bryte eksisterende funksjonalitet\n3. Auto-nedlasting basert på brukerens posisjon\n\nTekniske vurderinger:\n- ShelterDataSource-grensesnitt med per-land-implementasjoner\n- Parsefeil i én kilde må aldri ødelegge andre kilder (isolert per kilde, valider per record)\n- Generalisere Shelter-modellen (kjernefelt: koordinater WGS84, kapasitet, adresse, kildeland)\n- Bbox-basert dataset-registry, last bare ned relevante datasett\n- Offline-first beholdes — alle nedlastede datasett caches i Room\n\nOut of scope: brukerbidratte lokasjoner, sanntidsstatus, ruting.\n\nForgejo: https://kode.naiv.no/olemd/tilfluktsrom/issues/9 (3 kommentarer)","status":"open","priority":3,"issue_type":"feature","owner":"olemd@glemt.net","created_at":"2026-04-29T13:56:34Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-29T13:56:34Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt index e862589..a08bb49 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt @@ -44,6 +44,7 @@ import no.naiv.tilfluktsrom.location.ShelterFinder import no.naiv.tilfluktsrom.location.ShelterWithDistance import no.naiv.tilfluktsrom.ui.CivilDefenseInfoDialog import no.naiv.tilfluktsrom.ui.ShelterListAdapter +import no.naiv.tilfluktsrom.ui.ShelterListItem import no.naiv.tilfluktsrom.util.DistanceUtils import org.osmdroid.util.GeoPoint import org.osmdroid.views.CustomZoomButtonsController @@ -486,25 +487,60 @@ class MainActivity : AppCompatActivity(), SensorEventListener { allShelters, location.latitude, location.longitude, NEAREST_COUNT ) - // Highlight which nearest-list item matches the current selection - val selectedIdx = if (selectedShelter != null) { - nearestShelters.indexOfFirst { it.shelter.lokalId == selectedShelter!!.shelter.lokalId } - } else -1 - - shelterAdapter.submitList(nearestShelters) - shelterAdapter.selectPosition(selectedIdx) - if (userSelectedShelter && selectedShelter != null) { // Recalculate distance/bearing for the user's picked shelter refreshSelectedShelterDistance(location) + rebuildShelterList() + updateSelectedShelterUI() + } else if (nearestShelters.isNotEmpty()) { + // Auto-select nearest; selectShelter handles list rebuild + UI + selectShelter(nearestShelters[0]) } else { - // Auto-select nearest - if (nearestShelters.isNotEmpty()) { - selectShelter(nearestShelters[0]) - } + rebuildShelterList() + updateSelectedShelterUI() + } + } + + /** + * Rebuild the bottom-sheet list from the current nearest set + selection. + * + * Hybrid behaviour for Forgejo #13: when a shelter has been explicitly + * selected (deep link, marker tap, ...) and is *not* among the N nearest, + * append it to the list with an "outside nearest" badge so the user can + * see what they selected. The list also auto-scrolls to the selected + * row, so a manually-picked nearby entry comes into view too. + */ + private fun rebuildShelterList() { + val items = nearestShelters + .map { ShelterListItem(it, isOutsideNearest = false) } + .toMutableList() + + val selected = selectedShelter + val isSelectedAmongNearest = selected != null && + nearestShelters.any { it.shelter.lokalId == selected.shelter.lokalId } + if (selected != null && !isSelectedAmongNearest) { + // Only flag as "outside nearest" when there *is* a nearest list to + // contrast with - otherwise the selection is just the only entry. + items.add( + ShelterListItem( + selected, + isOutsideNearest = nearestShelters.isNotEmpty() + ) + ) } - updateSelectedShelterUI() + shelterAdapter.submitList(items) + + val selectedIdx = if (selected != null) { + items.indexOfFirst { it.swd.shelter.lokalId == selected.shelter.lokalId } + } else -1 + shelterAdapter.selectPosition(selectedIdx) + + if (selectedIdx >= 0) { + binding.shelterList.post { + binding.shelterList.smoothScrollToPosition(selectedIdx) + } + } } /** @@ -515,10 +551,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener { selectedShelter = swd currentLocation?.let { refreshSelectedShelterDistance(it) } - // Update list highlight - val idx = nearestShelters.indexOfFirst { it.shelter.lokalId == swd.shelter.lokalId } - shelterAdapter.selectPosition(idx) - + rebuildShelterList() updateSelectedShelterUI() } diff --git a/app/src/main/java/no/naiv/tilfluktsrom/ui/ShelterListAdapter.kt b/app/src/main/java/no/naiv/tilfluktsrom/ui/ShelterListAdapter.kt index f4a7307..e33eb85 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/ui/ShelterListAdapter.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/ui/ShelterListAdapter.kt @@ -2,6 +2,7 @@ package no.naiv.tilfluktsrom.ui import android.view.HapticFeedbackConstants import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -11,12 +12,23 @@ import no.naiv.tilfluktsrom.databinding.ItemShelterBinding import no.naiv.tilfluktsrom.location.ShelterWithDistance import no.naiv.tilfluktsrom.util.DistanceUtils +/** + * One row in the bottom-sheet list. The list normally holds the N nearest + * shelters to the user, but a deep-linked / explicitly-selected shelter that + * is *not* among them is appended with isOutsideNearest=true so the user can + * see what they picked. See Forgejo #13 / beads tilfluktsrom-9sf. + */ +data class ShelterListItem( + val swd: ShelterWithDistance, + val isOutsideNearest: Boolean +) + /** * Adapter for the list of nearest shelters shown in the bottom sheet. */ class ShelterListAdapter( private val onShelterSelected: (ShelterWithDistance) -> Unit -) : ListAdapter(DIFF_CALLBACK) { +) : ListAdapter(DIFF_CALLBACK) { private var selectedPosition = 0 @@ -42,23 +54,34 @@ class ShelterListAdapter( private val binding: ItemShelterBinding ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: ShelterWithDistance, isSelected: Boolean) { + fun bind(item: ShelterListItem, isSelected: Boolean) { val ctx = binding.root.context - binding.shelterAddress.text = item.shelter.adresse - binding.shelterDistance.text = DistanceUtils.formatDistance(item.distanceMeters) + val swd = item.swd + binding.shelterAddress.text = swd.shelter.adresse + binding.shelterDistance.text = DistanceUtils.formatDistance(swd.distanceMeters) binding.shelterCapacity.text = ctx.getString( - R.string.shelter_capacity, item.shelter.plasser + R.string.shelter_capacity, swd.shelter.plasser ) binding.shelterRoomNr.text = ctx.getString( - R.string.shelter_room_nr, item.shelter.romnr + R.string.shelter_room_nr, swd.shelter.romnr ) - binding.root.contentDescription = ctx.getString( + binding.outsideNearestBadge.visibility = + if (item.isOutsideNearest) View.VISIBLE else View.GONE + + // Build accessible description; suffix the badge text so screen- + // reader users learn the same context that sighted users see. + val baseDesc = ctx.getString( R.string.content_desc_shelter_item, - item.shelter.adresse, - DistanceUtils.formatDistance(item.distanceMeters), - item.shelter.plasser + swd.shelter.adresse, + DistanceUtils.formatDistance(swd.distanceMeters), + swd.shelter.plasser ) + binding.root.contentDescription = if (item.isOutsideNearest) { + ctx.getString(R.string.shelter_outside_nearest_badge) + ". " + baseDesc + } else { + baseDesc + } binding.root.isSelected = isSelected binding.root.alpha = if (isSelected) 1.0f else 0.7f @@ -68,18 +91,18 @@ class ShelterListAdapter( val pos = adapterPosition if (pos != RecyclerView.NO_POSITION) { selectPosition(pos) - onShelterSelected(getItem(pos)) + onShelterSelected(getItem(pos).swd) } } } } companion object { - private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) = - a.shelter.lokalId == b.shelter.lokalId + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(a: ShelterListItem, b: ShelterListItem) = + a.swd.shelter.lokalId == b.swd.shelter.lokalId - override fun areContentsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) = + override fun areContentsTheSame(a: ShelterListItem, b: ShelterListItem) = a == b } } diff --git a/app/src/main/res/layout/item_shelter.xml b/app/src/main/res/layout/item_shelter.xml index d150b81..e51d2ea 100644 --- a/app/src/main/res/layout/item_shelter.xml +++ b/app/src/main/res/layout/item_shelter.xml @@ -9,6 +9,21 @@ android:paddingHorizontal="12dp" android:paddingVertical="8dp"> + + Retning til tilfluktsrom, %s unna %1$s, %2$s, %3$d plasser + Valgt – utenfor nærområdet Upresist kompass - %s Tilfluktsromkart Kompassnavigasjon diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 5b809b8..67edcff 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -62,6 +62,7 @@ Retning til tilfluktsrom, %s unna %1$s, %2$s, %3$d plassar + Vald – utanfor nærområdet Upresis kompass - %s Tilfluktsromkart Kompassnavigasjon diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b622fa..1825950 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,9 @@ Direction to shelter, %s away %1$s, %2$s, %3$d places + + Selected (outside nearest) Low accuracy - %s Shelter map Compass navigation