d418f156fa
- Date pickers (entry form + edit dialog) now display TT.MM.JJJJ instead of
ISO (DATE_DISPLAY_FORMAT). Storage stays YYYY-MM-DD; parse_date() already
accepted both formats, so existing data and the self-updater are unaffected.
- New optional "Straße" field (street + house number) in the entry form and
edit dialog, backed by a new `street` CSV column. geocode_city() and
GeocoderWorker.enqueue() gained a street parameter: when set, a full-address
query is tried first for a much more precise map point, falling back
automatically to the existing city/PLZ search if it doesn't resolve.
- Tab 2 and the entry queue show a Straße column; Tab 2 search now also
matches on street.
- Fix: pandas turns a blank CSV cell into NaN even for a dtype=str column, so
every existing (blank-street) row would have shown literal "nan" in Tab 2.
DataStore._load now does street.fillna("") after every read.
Verified with a non-GUI test suite (date parsing, query construction, CSV
round-trip incl. the NaN case) and a full GUI build/drive test on Python
3.12/Tk 9. Docs updated (changelog, overview, architecture, data-model,
dev-notes, improvements).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
6.4 KiB
Markdown
121 lines
6.4 KiB
Markdown
# Overview – what the app does
|
||
|
||
There is a **Hilfe** menu with "Nach Update suchen" (self-update via `git`, see
|
||
[setup.md](setup.md#updating)) and "Version…".
|
||
|
||
The app is a single window: a **DRK-red header bar**, two tabs, and a
|
||
**colour-coded status bar** at the bottom (grey = neutral, green = success,
|
||
orange = warning, red = error; transient messages fade back to neutral after
|
||
~8 s). The window title shows the total entry count, and the window remembers
|
||
its size and position between sessions (`data/window.json`). The UI font is
|
||
enlarged for readability (`UI_FONT_SIZE`).
|
||
|
||
## Tab 1 – "Neuer Eintrag" (new entry)
|
||
|
||
Workflow: build up a **queue** of entries, let them geocode in the background,
|
||
then save the whole batch at once.
|
||
|
||
1. **Entry form** – date (a **calendar picker**, `ttkbootstrap.DateEntry`,
|
||
pre-filled with today and **displayed as `TT.MM.JJJJ`**, e.g. `12.09.2026`;
|
||
typed input also accepts `JJJJ-MM-TT` and is **validated** – an invalid date
|
||
is rejected with a dialog), city (autocomplete combobox), postal code
|
||
(optional, digits only), and **Straße** (street + house number, optional,
|
||
second row of the form) — see [Geocoding behaviour](#geocoding-behaviour) for
|
||
why it's worth filling in. Pressing `Return` in any field, or the
|
||
**+ Hinzufügen** button, adds the row to the queue.
|
||
2. **Queue table** – shows date, city, PLZ, street, and a live coordinate column
|
||
that updates from `⏳ wird gesucht…` to either `lat / lon` or
|
||
`⚠ nicht gefunden` as the background geocoder works through the queue.
|
||
- Select a row and press <kbd>Delete</kbd>, use the **"Auswahl entfernen"**
|
||
button, or click the `✕` cell.
|
||
- Right-click a row for *Entfernen* / *Koordinaten erneut suchen*.
|
||
3. **Action bar**
|
||
- **Alle speichern** – appends every queued row to `orte.csv`. If any row
|
||
looks like a duplicate (same date + city + PLZ), a **modal** lists them and
|
||
asks whether to save anyway.
|
||
- **Warteschlange leeren** – discards the queue (asks for confirmation).
|
||
- **Karte öffnen** – regenerates `karte.html` and opens it in the browser.
|
||
|
||
## Tab 2 – "Einträge verwalten" (manage entries)
|
||
|
||
A table view of everything in `orte.csv`.
|
||
|
||
- **Columns:** Datum, Ort, PLZ, Straße, and **Karte** – a status column showing
|
||
`✓` when the row has coordinates or a red `fehlt` when it doesn't (the whole
|
||
row is red too). The raw lat/lon numbers live in the edit dialog, not this
|
||
table.
|
||
- **Search box** – live filter across date, city, PLZ, **and street**
|
||
(substring match).
|
||
- **Sortable columns** – click a header to sort; clicking again reverses. The
|
||
active column shows a ▲/▼ arrow. Default sort is by date, newest first.
|
||
Sorting by **Karte** groups the rows without coordinates together.
|
||
- **"Nur ohne Koordinaten"** toggle – filters to entries that have no
|
||
coordinates yet. The hint line always shows the total count, plus how many are
|
||
missing coordinates.
|
||
- **Edit** – double-click a row (or *Bearbeiten*) opens a modal dialog to change
|
||
date (calendar picker, `TT.MM.JJJJ`, validated) / city / PLZ / **Straße**. Two
|
||
ways to fix coordinates: a "Koordinaten automatisch neu suchen" toggle re-runs
|
||
geocoding after saving (now using the street if one is set), or the
|
||
**Breitengrad / Längengrad** fields let you type them in by hand (both empty =
|
||
no map marker).
|
||
- **Right-click → Koordinaten suchen** – runs geocoding for that one row
|
||
(handy for rows that failed the first time).
|
||
- **Delete** – *Löschen* removes the selected row(s) after a confirmation
|
||
dialog. This is **irreversible**, but a timestamped copy of the file is
|
||
written to `data/backups/` before every change (see
|
||
[data-model.md](data-model.md#backups)).
|
||
- **Karte öffnen** – same as on Tab 1.
|
||
|
||
## The map (`karte.html`)
|
||
|
||
Generated by `generate_map()`:
|
||
|
||
- Base layer: **Esri "World Light Gray"** (`MAP_TILES` + a labels overlay) – a
|
||
light, muted basemap that needs no API key and no `Referer`. Initial view
|
||
centred on `[49.0, 9.0]`, zoom 8; auto-fits to the markers when there is more
|
||
than one.
|
||
- One **`CircleMarker`** per unique `(city, lat, lon)` group. Radius scales
|
||
linearly with the visit count for that location
|
||
(`5 + count / max_count * 20` px), colour is DRK red (`#CC0000`).
|
||
- Popup shows the city and the visit count; tooltip shows the city.
|
||
- Rows with missing/blank coordinates are silently excluded.
|
||
- If there are no usable coordinates at all, a dialog says so (and the status
|
||
bar shows a warning).
|
||
|
||
> The generated HTML pulls Leaflet, jQuery and Bootstrap from CDNs, and the map
|
||
> tiles from `server.arcgisonline.com`, so the map needs an internet connection
|
||
> to render even though the data is local. To change the look, edit `MAP_TILES` /
|
||
> `MAP_TILES_LABELS` near the top of `app.py` — the comment there lists other
|
||
> no-key Esri layers (Street, Topo). `tile.openstreetmap.org` was tried first
|
||
> but its tile-usage policy **403s** pages opened from `file://` (no `Referer`);
|
||
> `CartoDB positron` needs an API key.
|
||
|
||
## Geocoding behaviour
|
||
|
||
`geocode_city(city, postal_code, street="")` tries a series of queries in order
|
||
and returns the **first hit**:
|
||
|
||
1. `"<street>, <PLZ> <city>, Germany"` — only if a **street** was given (with
|
||
PLZ if there is one, without it if not). This is what the optional Straße
|
||
field is for: a full address geocodes far more precisely than a city name
|
||
alone (a point in the right town vs. the right building).
|
||
2. `"<PLZ> <city>, Germany"` (only if a PLZ was given)
|
||
3. `"<city>, Baden-Württemberg, Germany"`
|
||
4. `"<city>, Hessen, Germany"`
|
||
5. `"<city>, Germany"`
|
||
|
||
If the street-level query (1) doesn't match anything in Nominatim (typo, house
|
||
number Nominatim doesn't know, etc.), it **falls back automatically** to the
|
||
same city/PLZ-level searches used when no street is given — a bad street never
|
||
makes geocoding *worse* than before, only better when it resolves.
|
||
|
||
The regional bias is the `GEOCODE_REGIONS` constant (`Baden-Württemberg`,
|
||
`Hessen`) near the top of `app.py`. Results are rounded to five decimal places.
|
||
Nominatim's usage policy (max 1 req/s, identifying `user_agent`) is respected by
|
||
a manual `time.sleep(1)` in the worker loop.
|
||
|
||
If the geocoding service is unreachable (no internet), entries still save –
|
||
without coordinates – and after a few consecutive failures a one-time dialog
|
||
explains that the coordinates can be added later via *Bearbeiten*. The worker
|
||
thread logs failures to `data/app.log` and keeps running.
|