First version of Karten App

This commit is contained in:
2026-10-09 04:20:40 +02:00
commit 83a36c7a22
41 changed files with 10834 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
# Erste Version der Karte
Status: done
## Problem Statement
The DTK25 Blätter are on the Mac as separate sheets. On a phone or tablet they cannot be panned, zoomed, or used once the computer is left behind. A person in the terrain needs the Karte, one Ziel entered as a Koordinate, and their own Standort when the device can provide it.
## Solution
An installable German app for phone and tablet shows the Karte, keeps downloaded Kacheln on the device, and works offline after that. The person types one Koordinate, sees that Ziel on the Karte, and sees their Standort and the straight-line distance when both exist. Hessen is the one Gebiet they can download. The Übersicht ships with the app, so the Karte knows where a Kachel exists before its pixels are on the device.
## User Stories
1. As a person using the Karte, I want to open it on my phone or tablet, so that I can read the DTK25 sheets in the terrain.
2. As a person using the Karte, I want the app in German, so that the words match the Koordinate I type.
3. As a person using the Karte, I want to install the app, so that I can open it without an app store.
4. As a person using the Karte, I want the app to start offline after the Kacheln I need are on the device, so that I can work with the computer left behind.
5. As a person using the Karte, I want Koordinate conversion to work offline, so that I can name a Ziel with no network.
6. As a person using the Karte, I want only the Kacheln inside the current view to be drawn, so that the phone does not load the whole Karte at once.
7. As a person using the Karte, I want the Übersicht to be present as soon as the app is installed, so that the Karte knows which Kacheln exist before I download their pixels.
8. As a person using the Karte, I want a Kachel that is already in the Cache to be drawn, so that I see the map I already have.
9. As a person using the Karte, I want a Kachel the Übersicht knows, that is not in the Cache, to appear blank and then download while I am online and the app is open, so that the place I am looking at fills in.
10. As a person using the Karte, I want that download to finish and stay in the Cache even if I pan away before it ends, so that the Kachel is there when I come back.
11. As a person using the Karte, I want a Kachel the Übersicht knows, that is not in the Cache, to stay blank when I am offline, and to be told it is not on the device, so that a blank view is not presented as a finished map.
12. As a person using the Karte, I want that offline Kachel to download the next time the app is open, the device is online, and the Karte needs it, so that I do not have to start it by hand.
13. As a person using the Karte, I want a place the Übersicht does not know to stay blank and never download, so that I am not offered map data that does not exist.
14. As a person using the Karte, I want the view to open at 1:25,000, so that the sheets look like the printed map.
15. As a person using the Karte, I want the Karte to stay north-up, so that the sheets keep their usual orientation.
16. As a person using the Karte, I want to pan with touch, so that I can move across the Karte.
17. As a person using the Karte, I want to pinch-zoom, so that I can see more ground or more detail.
18. As a person using the Karte, I want pinch-zoom to stop at 36 km by 36 km, so that the view stays within a readable span.
19. As a person using the Karte, I want that furthest span to be the same on phone and tablet, so that both show the same ground limit.
20. As a person using the Karte, I want pinch-zoom to stop at one sheet pixel, 1.25 m on the ground, so that I cannot zoom past the detail in the Blätter.
21. As a person preparing the app, I want the 36 km span to be a value shipped with the app, so that I can tune it without preparing the Kacheln again.
22. As a person using the Karte, I want the opening view to center on my Standort when the device already has one, so that I see where I am.
23. As a person using the Karte, I want my Standort drawn in blue, so that I can tell it from the Ziel.
24. As a person using the Karte, I want the blue Standort to move as I move, so that it stays my current position.
25. As a person using the Karte, I want the view to stay where I left it while the Standort moves, so that I can pan and read the Karte.
26. As a person using the Karte, I want the view, when I have no Standort and the Cache already holds Kacheln, to open on the middle of those Kacheln, so that I see map I already have.
27. As a person using the Karte, I want the view, when I have no Standort and the Cache is empty, to open on the middle of the Übersicht, so that I am not looking at an undefined place.
28. As a person using the Karte, I want the first Standort, if it arrives after opening and I have not panned or entered a Ziel, to center the view once, so that the late fix still does the opening center.
29. As a person using the Karte, I want a late first Standort, after I have panned or entered a Ziel, to move only the blue mark, so that my place on the Karte is not yanked away.
30. As a person using the Karte, I want a denied Standort to leave the view on its opening center, so that refusing the device position still shows a Karte.
31. As a person using the Karte, I want to center on my Standort on purpose, at 1:25,000, so that I can return to myself after looking elsewhere.
32. As a person using the Karte, I want to center on the Ziel on purpose, at 1:25,000, so that I can return to it after panning.
33. As a person using the Karte, I want to type a Koordinate as Gitter, Rechtswert, and Hochwert, so that I can name a Ziel in the MGRS form I already use.
34. As a person using the Karte, I want the Gitter field to start as `32U MA`, so that the usual square is ready.
35. As a person using the Karte, I want to type a different Gitter, so that I can name a Ziel on any square the sheets cover.
36. As a person using the Karte, I want `32u ma` and `32U MA` to be the same Gitter, so that letter case does not reject a valid Koordinate.
37. As a person using the Karte, I want `32U MA` and `32UMA` to be the same Gitter, so that a missing space does not reject a valid Koordinate.
38. As a person using the Karte, I want a Rechtswert and a Hochwert of exactly five digits, so that the Koordinate is a 1 m cell.
39. As a person using the Karte, I want a shorter or longer number rejected, with a clear German explanation, so that I know the Koordinate was not accepted.
40. As a person using the Karte, I want an impossible Gitter rejected, with a clear German explanation, so that a bad combination is not drawn.
41. As a person using the Karte, I want the Koordinate I typed to stay visible, so that I can check it against the red mark.
42. As a person using the Karte, I want a valid Ziel drawn in red and the view centered on it at 1:25,000, so that I can see the place I named.
43. As a person using the Karte, I want that centering to happen even while the Kacheln are still blank, so that the Ziel is not refused just because the pixels are still arriving.
44. As a person using the Karte, I want a second Koordinate to replace the first, so that there is only one Ziel.
45. As a person using the Karte, I want a rejected Koordinate to leave the previous Ziel and the view unchanged, so that a mistake does not wipe the place I already had.
46. As a person using the Karte, I want a Koordinate that no Blatt covers to be refused, so that I am not sent to a hole or past the sheets.
47. As a person using the Karte, I want the Ziel to disappear when I leave or reload the page, so that this version does not keep a Ziel I did not ask to store.
48. As a person using the Karte, I want the straight-line distance between Standort and Ziel when both exist, so that I know how far the Ziel is.
49. As a person using the Karte, I want that distance in metres below 1000 m and in kilometres from 1000 m, so that the number stays readable.
50. As a person using the Karte, I want the distance to update as the Standort moves, so that it stays current while I walk.
51. As a person using the Karte, I want the distance hidden when either the Ziel or the Standort is missing, so that I am not shown a distance to nowhere.
52. As a person using the Karte, I want the distance while the Kacheln under the Ziel are still blank, so that the number does not wait for pixels.
53. As a person using the Karte, I want to see Hessen as a Gebiet I can download, so that I can put every current Kachel on the device without visiting each place.
54. As a person using the Karte, I want Hessen to contain every Kachel in the Übersicht, at every scale from the 36 km view down to the native sheet pixel, so that offline zoom has the pixels it needs.
55. As a person using the Karte, I want Hessen marked heruntergeladen when I start it, so that the choice counts before the last Kachel arrives.
56. As a person using the Karte, I want to see how much of Hessen has arrived, so that a long download is not a silent wait.
57. As a person using the Karte, I want the Kacheln of the current view to arrive before the rest of Hessen, so that I can read the place I am looking at while the rest continues.
58. As a person using the Karte, I want to pan and zoom offline after Hessen has fully arrived, so that the Karte works with no network.
59. As a person using the Karte, I want on-demand download, when Hessen is not heruntergeladen, to fetch only the Kacheln the current view needs at the current scale, so that looking around does not pull the whole Gebiet.
60. As a person using the Karte, I want Kacheln that have arrived to stay on the device, so that a dropped connection does not throw away finished work.
61. As a person using the Karte, I want a paused Hessen download to continue the next time the app is open and the device is online, so that I do not start from zero.
62. As a person using the Karte, I want downloads to pause when I leave the app, so that the phone does not keep fetching with the app closed.
63. As a person using the Karte, I want no download while the device is offline, so that the Karte waits until a connection exists.
64. As a person using the Karte, I want to keep using Kacheln already in the Cache while Hessen is still downloading, so that the finished part of the Karte is usable.
65. As a person using the Karte, I want to be asked before Hessen is removed, with Hessen named in the question, so that I do not wipe it by mistake.
66. As a person using the Karte, I want removing Hessen to stop the rest of its download, so that a cancelled Gebiet does not keep fetching.
67. As a person using the Karte, I want removing Hessen to remove its Kacheln from the Cache, so that the space can be given back.
68. As a person using the Karte, I want a Kachel that also lies in another Gebiet still marked heruntergeladen to remain, so that removing one Gebiet does not break another.
69. As a person using the Karte, I want a Kachel that arrived only because I looked at it to be removed with the Gebiet that contains it, when no other heruntergeladenes Gebiet contains it, so that panning is not a second way to keep Hessen.
70. As a person using the Karte, I want to be asked before the whole Cache is deleted, with the whole Cache named in the question, so that I do not wipe every Kachel by mistake.
71. As a person using the Karte, I want deleting the whole Cache to remove every Kachel and clear every heruntergeladen mark, so that the device matches an empty start.
72. As a person using the Karte, I want the current view, after the whole Cache is deleted while I am online, to fill again only by on-demand download, so that Hessen does not start itself again.
73. As a person using the Karte, I want to start Hessen again after I have removed it, so that a delete is reversible by downloading once more.
74. As a person using the Karte, I want the Standort to be the device position only, and only while the app is open, so that this version has one blue mark and no tracking with the app closed.
75. As a person preparing the Karte, I want the Blätter turned into Kacheln once on the Mac, so that the phone never has to read the original sheets.
76. As a person preparing the Karte, I want those Kacheln and the app published as static files, so that the phone can download them with no live map server.
## Implementation Decisions
- Follow ADR 0001 and ADR 0002. The app is an installable PWA. The Mac prepares Kacheln once. The phone loads them from static hosting. There is no live map server and no runtime dependency on an external CDN.
- The screen talks only to a Karte session. That session is the one test seam. It accepts a Koordinate, a Standort report, online or offline, whether the app is open, pan and zoom, center-on-Standort, center-on-Ziel, start of a Gebiet, removal of a Gebiet, and deletion of the whole Cache. It exposes the view center and scale, the Ziel, the Standort, the distance, the German notice, the Koordinate text, the heruntergeladen marks and their progress, and the appearance of each Kachel in the current view: drawn, blank and downloading, blank and not on the device, or blank because the Übersicht does not know it.
- Map drawing, the browser position API, and Kachel byte storage sit behind the session. The session does not know about a map library or a browser.
- The UI is German. The field labels are Gitter, Rechtswert, and Hochwert. The Gebiet the screen offers is Hessen. The Ziel mark is red. The Standort mark is blue.
- A Koordinate is parsed as MGRS and then transformed into EPSG:25832. The metres inside the Gitter are not treated as already being EPSG:25832.
- The Gitter is a zone number, a latitude band, and a 100 km square. Surrounding space is ignored. Letter case is ignored. Rechtswert and Hochwert are exactly five digits, including a leading zero. Any other shape, or a combination that is not a real MGRS pair, is rejected. The notice explains the rejection in German. The previous Ziel and the view stay as they were.
- A decoded Koordinate is rejected when no Blatt covers the point, including a hole between Blätter. The previous Ziel and the view stay as they were.
- A valid Ziel replaces any previous Ziel, is kept only in the session, and is dropped when the session ends. The view centers on it at 1:25,000 even when its Kacheln are blank.
- The typed Gitter, Rechtswert, and Hochwert stay on screen after a successful entry.
- The Standort is a position handed to the session by the device adapter, or an explicit denial. The session does not ask the device itself. While the app is open, later positions move the Standort and the distance. They move the view only for the first position, and only when the person has not panned and has not entered a Ziel since opening.
- Distance is the planar distance in EPSG:25832. Below 1000 m it is metres. From 1000 m it is kilometres. It is absent unless both Ziel and Standort exist.
- Opening centers at 1:25,000. With a Standort already known, the center is that position. With none, and with Kacheln in the Cache, the center is the middle of what the Cache holds. With none and an empty Cache, the center is the middle of the Übersicht. A denied Standort uses that same fallback.
- The Karte stays north-up. Pinch-zoom stops at the configured ground span, default 36 km by 36 km, and at a ground resolution of 1.25 m per screen pixel. The span is data shipped with the app. Changing it does not require preparing the Kacheln again. The person using the phone cannot edit it.
- The Übersicht is a static catalog shipped with the app. It lists every Kachel at every prepared scale and the Blatt coverage used to accept or refuse a Koordinate. It contains no pixels.
- Kachel bytes live in the device cache used by the installed app. Heruntergeladen marks and download progress live in the device database. Both survive leaving the app. The Ziel does not.
- Prepared scales run from the configured furthest view down to the native sheet pixel. Each coarser scale covers four times the ground of the next finer one. Hessen is the set of every such Kachel in the Übersicht.
- Starting Hessen marks it heruntergeladen immediately and queues every scale. Kacheln needed by the current view are fetched first, then the rest. The session exposes progress. An on-demand fetch, used when the view needs a Kachel and Hessen is not heruntergeladen, queues only that view at the current scale.
- Fetches run only when the session is told that the app is open and the device is online. Closing the app pauses them. Opening it again continues an unfinished Gebiet and any Kachel the current view still needs.
- Removing a Gebiet stops its remaining fetches, then deletes every cached Kachel that lies in it and lies in no other Gebiet still marked heruntergeladen. Kacheln that arrived by viewing have no extra protection. Deleting the whole Cache deletes every Kachel, clears every heruntergeladen mark, and does not start Hessen again. A later on-demand fetch may refill the current view if the device is online.
- The screen asks for confirmation before it tells the session to remove a Gebiet or delete the Cache. The question names the Gebiet, or names the whole Cache. The session itself applies the delete when asked. Tests drive the session after that confirmation.
- The session's Gebiet rules are general, so overlap can be tested by giving it two Gebiete. The screen in this version offers only Hessen. Usingen and Wehrheim are the overlap examples from ADR 0002, not Gebiete the screen can create.
- The Mac preparation reads the Blätter on hand, checks them against 12 km sheets of 9600 by 9600 pixels at 1.25 m in EPSG:25832, and writes the Kacheln plus the Übersicht. The phone never reads the original sheets.
- The stack is TypeScript and Vite for the app, OpenLayers for drawing, proj4js plus a separate MGRS library for the Koordinate, a service worker for the app shell, Cache Storage for Kachel bytes, and IndexedDB for marks and progress. Python and GDAL prepare the Kacheln. Library versions and the static host are chosen at implementation time. They must satisfy ADR 0001.
- An installed app should request persistent device storage so the Cache is not cleared out from under a finished Hessen download.
## Testing Decisions
- A good test drives the Karte session the way the screen will, and asserts what the screen would show. It does not assert how a Kachel is stored, how a library is called, or how a map is drawn.
- The only module under test is the Karte session. There is no prior test suite in this repo.
- Tests supply a fake Übersicht, a fake Cache, a fake clock for progress, and Standort and online reports. They do not open a browser, run GDAL, or draw with OpenLayers.
- Cover the four Kachel appearances, opening centers, the late first Standort, Koordinate acceptance and both kinds of refusal, replacement of the Ziel, distance and its absence, Hessen progress and view-first order, pause and resume, the overlap delete, and deletion of the whole Cache.
- Overlap is tested by handing the session two Gebiete even though the screen offers only Hessen.
- The German confirmation question is specified as screen copy. It is not a second test seam. Session tests start from a delete the screen has already confirmed.
## Out of Scope
- Storing the Ziel, and any list of recent coordinates.
- MGRS strings with fewer than five digits on each axis, and any drawing of a larger cell.
- Entering a Ziel as UTM or WGS84.
- Typing a Standort by hand.
- Routing, turn-by-turn guidance, and a Standort update while the app is closed.
- Downloads while the app is closed.
- Defining new Gebiete in the app. The overlap rule is in the session; the screen offers Hessen only.
- A control for the 36 km span on the phone.
- A macOS desktop app, a live map server, and publishing through an app store.
## Further Notes
- The sample Koordinate `32U MA 67030 78109` decodes to UTM zone 32, easting 467030 m, northing 5578109 m, and a Blatt in the Übersicht covers it.
- The Blätter on hand are about 1.3 GB. The finest Kacheln are most of the prepared size. The coarser scales add about a third more.
- Hessen publishes these DTK25 sheets as open data, so preparing them for this app is allowed.
- `Erste Planung.md` is the wider note from before this cut. Where it disagrees with this spec, this spec is the first version.
- ADR 0001 and ADR 0002 are accepted. This spec does not reopen them.