Vis dyplenket tilfluktsrom i lista, selv utenfor topp-3 (hybrid)

Når en dyplenke (eller markørtap) velger et tilfluktsrom som ikke er
blant de N nærmeste, blir det nå appendet til bunnpanelets liste med
et tydelig "Valgt – utenfor nærområdet"-badge, og lista scroller til
den valgte raden. Hvis valget er innenfor topp-N, scrollelementet
fortsatt synes — løser begge symptomene som rapporten pekte på.

Endringer:
- Ny ShelterListItem(swd, isOutsideNearest)-wrapper i adapteren
- ShelterListAdapter: viser badge + a11y-suffiks når isOutsideNearest=true
- item_shelter.xml: badge-TextView (orange bakgrunn, hvit tekst, gone som default)
- MainActivity: rebuildShelterList()-helper bygger top-N + maybe-appended,
  smoothScrollToPosition(selectedIdx) sikrer synlig markering
- Strings i en/nb/nn

Forgejo: #13
This commit is contained in:
Ole-Morten Duesund 2026-04-29 16:49:28 +02:00
commit 1fb9f14ad4
7 changed files with 109 additions and 33 deletions

View file

@ -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}

View file

@ -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()
}

View file

@ -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<ShelterWithDistance, ShelterListAdapter.ViewHolder>(DIFF_CALLBACK) {
) : ListAdapter<ShelterListItem, ShelterListAdapter.ViewHolder>(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<ShelterWithDistance>() {
override fun areItemsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) =
a.shelter.lokalId == b.shelter.lokalId
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ShelterListItem>() {
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
}
}

View file

@ -9,6 +9,21 @@
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/outsideNearestBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:background="@color/shelter_primary"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:text="@string/shelter_outside_nearest_badge"
android:textColor="@color/white"
android:textSize="11sp"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/shelterAddress"
android:layout_width="wrap_content"

View file

@ -62,6 +62,7 @@
<!-- Tilgjengelighet -->
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plasser</string>
<string name="shelter_outside_nearest_badge">Valgt utenfor nærområdet</string>
<string name="compass_accuracy_warning">Upresist kompass - %s</string>
<string name="a11y_map">Tilfluktsromkart</string>
<string name="a11y_compass">Kompassnavigasjon</string>

View file

@ -62,6 +62,7 @@
<!-- Tilgjenge -->
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plassar</string>
<string name="shelter_outside_nearest_badge">Vald utanfor nærområdet</string>
<string name="compass_accuracy_warning">Upresis kompass - %s</string>
<string name="a11y_map">Tilfluktsromkart</string>
<string name="a11y_compass">Kompassnavigasjon</string>

View file

@ -62,6 +62,9 @@
<!-- Accessibility -->
<string name="direction_arrow_description">Direction to shelter, %s away</string>
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d places</string>
<!-- Badge shown on a list row that is not among the nearest shelters but
was explicitly selected (e.g. via a deep link). Forgejo #13. -->
<string name="shelter_outside_nearest_badge">Selected (outside nearest)</string>
<string name="compass_accuracy_warning">Low accuracy - %s</string>
<string name="a11y_map">Shelter map</string>
<string name="a11y_compass">Compass navigation</string>