Planner now tries a route over roads you have actually ridden before
falling back to BRouter public data:
- Rust get_ride_graph(min_speed): nodes snapped to ~20m grid (walk=1
points excluded), edges between consecutive same-session points with
weight = node-to-node distance (consecutive-point distance loses
intra-cell travel and makes Dijkstra underestimate).
- JS loadRideGraph cache, grid-indexed nearest-node, Dijkstra, rideRoute
(snap <=60m, drop if >2.5x straight line).
- routeDirty: rideRoute first, BRouter(+nogos) in the gaps. Ridden leg
has no elevation -> thin line; BRouter -> casing (visually distinct).
- formatDistance now rounds metres (was 966.145...m).
Stage B of the ridden-roads work. GPS.detectWalk() flags contiguous low-speed
runs (< walkDetectKmh, or the single-speed 30-rpm push speed, for >= walkMinRunSec)
as walking inside a bike/other ride. Persisted per point (gps_points.walk, via
DB.setWalkFlags in recalculateSession + recomputeAllStats). rebuild_roads maps a
walk point to atype 'walk' so it colours as walking and is not treated as a
rideable road. Track detail colours walk runs and shows 'пешком: N мин / X км'.
Verified detectWalk on real tracks: 98% on a mostly-pushed 4km ride vs 1% on a
normal 16km ride.
Rust export_image(filename,base64,action) + read_file_base64(path) (registered).
Kotlin exportimg: branch — base64-decode, then SAF-save the PNG (CreateDocument
image/png) or share it via FileProvider + ACTION_SEND chooser. Settings gets a
'Фон для картинки' row: pick a photo (pickLocalFile image/*), read it as base64,
downscale to <=1080 and store as a data URL in localStorage for the 'С фото'
variant. Rust compiles in tauri dev; native paths need an APK rebuild to test.
Stage 1 (pure JS). buildShareImage() draws a 9:16 story PNG: track shape (colored
glowing line), big Distance/Time/Speed + small elevation/kcal/date, on a
transparent or photo background. showShareModal() previews it with a
transparent/photo toggle and Share/Save buttons; a 'Похвастаться' button in the
track detail opens it. Native export (SAF save + share intent) and the settings
background picker come in Stage 2 (needs an APK rebuild); on desktop the export
buttons toast an error.
Stage 2. Recording-map button ↪ builds a detour from the current GPS position
back ONTO the active route past the blockage: reference = nearest nogo on the
route (else current pos), target = a route point ~1.5km further (steps further on
failure to the route end), avoiding nogos and 180° turns (bearing helper). Drawn
as a transient magenta navigation overlay with the rejoin point; long-press the
button clears it. Does not modify the saved route or interrupt recording.
Stage 1 of the nogo/reroute work. Mark impassable circles that BRouter routes
around, end to end:
- DB nogo_areas table + CRUD; drawNogoAreas() red circles (tap to remove).
- BRouter.route(points, profile, nogos): &nogos=lon,lat,r on the RouteServer and
nogos in the embedded-engine request; Rust brouter_route passes it through;
Kotlin builds OsmNodeNamed(radius,isNogo,nogoWeight=NaN) -> rc.nogopoints +
prepareNogoPoints (needs an APK rebuild for offline use).
- Planner: ⛔ button toggles nogo-placement (tap map/line), each leg re-routes
around nogos. Recording map: ⛔ marks the current spot impassable. Detail map
shows the circles.
Verified the RouteServer detours around a nogo (2873m -> 3329m).
Found in desktop testing: saving a built route showed 'Маршрут пуст' and dropped
to the list — addRoute returned the INSERT lastInsertId, an i64 the SQL plugin
corrupts by ±1, so showRouteDetail opened a non-existent id. Re-read the id via
CAST(id AS TEXT) (same guard importGpxSession uses). Session log: desktop
verification of all 6 fixes.
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
Found in desktop testing: showSessionDetail() from the save/revert handlers
pushed a second detail overlay whose map rendered blank. reopenDetail() collapses
the stack and re-renders in place (the pattern recalc/refine already use). Session
log updated with the desktop verification results.
Gap-fill (track detail): GPS.fillGaps now returns {points,gaps} with per-gap
status; the 🧩 control previews ONLY the filled dropouts (cyan casing; unfilled
= red dashed) with a Save/Cancel panel. Save is a reversible in-place patch
(estimated points added, original untouched; DB.saveGapFill/revertGapFill +
estimated/gap_filled columns), then recalculateSession. Revert removes them.
Route planner: renamed menu 'Нарисовать маршрут' → 'Нарисовать тренировку'
(it makes a workout session, not a route); routes empty-state now points at the
on-phone planner. showRouteEditor rewritten per-leg: each leg road (BRouter) or
straight independently, road by default, tap a leg to toggle it, long-press to
insert, silent straight fallback where no road; the global button sets all legs.
Remove the home-city name from docs/comments/UI strings and the external
mirror IP (WAN) from code, docs and the Android cleartext allowlist, replacing
the IP with mirror.invalid (set the real host locally, uncommitted). LAN mirror
and routing keep working; the WAN fallback needs the real host set. Generic
big-city map presets are kept (not identifying).
leaflet-hotline has a single outlineColor, so render the colored core thin
(outlineWidth 0) and add two plain polylines beneath it in a dedicated pane
just under the hotline pane: a wide black one and a narrower white one — black
rim, white rim, colored core, legible on any map background. Map captured via
L.Map addInitHook to build the casing pane.
- .height-focus.label (on-map hover box) lifted 42px so it no longer covers
the route line.
- Repoint the 'Route quality' overlay's incline provider from signed incline
(degrees, blue->green->red) to |grade %| with BalDav's discrete bands
(green->yellow->red->brown->black, >=10% black), via addInitHook before the
control is built; best-effort auto-enable the overlay.
The .focusbox tooltip rect stayed white and our .elevation text rule turned
its text light — light-on-light, unreadable on the dark chart. Give the box a
dark ground with light text (same for the on-line height label), and lighten
the near-black guide lines/markers so they show on the dark chart.
doBrouter() ended with changed(), which nulled routeGeom and re-queued
doBrouter — an endless re-route loop where the road line was never drawn
(editor kept the straight line, distance unchanged). Draw directly instead
(updatePolyline/updateStats).
Verified end-to-end in the UI on a virtual display (Xvfb) against the
RouteServer: editor 'По дорогам' turns a 1.69km straight line into a 2.25km
road-following route; the gap-fill '🧩' overlay draws a shortest-road cyan
dashed line across a track's GPS dropouts. Docs (README/PROJECT_MAP/plan/log)
updated to match; router.js/poi.js now listed in PROJECT_MAP.
Stage 4 — gap fill: GPS.fillGaps(points) routes each GPS dropout (dt>gap
threshold, chord>60m) with the shortest road path and splices the routed points
in (flagged estimated), falling back to the chord when the road is >3x the chord
(graph detoured). A 🧩 button on the track detail draws the road-filled path as
a cyan dashed overlay — non-destructive (stats recompute over filled points is a
follow-up). Also fixes doBrouter: parseRouteGPX returns {name,points}, not an
array (planning would have kept the straight line).
Release hardening: network_security_config.xml permits cleartext only to the
mirror/RouteServer hosts (base off, debug-overrides on) — wired via
android:networkSecurityConfig; proguard keeps btools.** so release minification
doesn't strip the embedded engine.
Stage 3 — planning: showRouteEditor gets a "🚲 По дорогам" toggle. Waypoints
stay editable markers; with it on, the line between them (and the saved
geometry) follows roads via BRouter (brouterMode/routeGeom, 450ms debounce on
edits). BRouter.route (router.js) now: Android -> embedded engine, otherwise
(desktop / at home) -> the PC RouteServer over HTTP (http_request bypasses the
WebView CSP; LAN 192.168.1.18:17777 / WAN mirror.invalid:17777). Lets planning be
debugged on the desktop.
Stage 2 — segment UI: a "Сегменты" button on the roads screen opens
showBrouterSegments — region tiles (E35_N45/E40_N45), "есть" status, a
Download/Update button with percent progress via BRouter.downloadTileAndWait.
Verified: RouteServer returns 83 trkpt for a 3-waypoint trekking route (3594 m),
parseRouteGPX reads trkpt; both files compile into the apk. End-to-end UI (tap
waypoints -> routed line; in-app segment download) pending desktop/phone run.
Cover the elements that stayed light: right vertical sidebar tab strip, search
fields, the profile dropdown and dropdown menus, light-styled buttons, the
elevation chart's SVG axis text (was black on dark) and its background/footer/
stats. Injected as a <style> keyed on body.bw-dark.
Points of interest (cafe, shops, water, toilets, bank/ATM, bus, shelter,
viewpoint, ...) as circular contrasting badges (emoji in a white/dark circle,
readable on light and dark tiles) — not pins. A 📍 control toggles the layer
and opens a per-category filter (checkboxes, remembered). Markers are culled to
the visible bounds and shown from zoom 13 (cap 500/view), on the recording,
track-detail and roads maps.
Data is bundled and fully offline: frontend/data/pois.json ([{la,lo,c,n}], 358KB,
5662 POIs) fetched once from Overpass for ~100km around region (the whole
bbox returned in one query; adaptive subdivision was ready if it had truncated).
Verified functionally on device: 124 badges rendered in view at z16 over
region with correct bounds/category culling; the live visual could not be
screenshotted (the WebView surface is black under CDP). frontend/data + poi.js.
The navbar toggle was invisible/zero-width; replaced with a fixed floating
button (bottom-right). Dark mode now injects a <style> keyed on body.bw-dark
covering the tile layer, right sidebar, left toolbar (zoom/draw controls) and
the bottom elevation profile panel (incl. the chart drawn later on route).
Static brouter-web on the Keenetic mirror pointed at the PC RouteServer.
config.js (host/layers/centre), baldav-custom.{css,js} (day/night toggle that
inverts only the tile layer + dark sidebar, one-time default POI overlays),
and a README with the deploy steps. Kept in-repo so the mirror is reproducible.
Two fixes to the dark map + offline tiles:
- Dark backdrop: body.dark-map also darkens .leaflet-container (was #ddd), so
the light Leaflet background no longer flashes bright between/behind tiles
while zooming or where a tile is missing.
- Tile caching: createTile fetched the display <img> from the URL but cached
via a SEPARATE fetch — on a flaky mobile link that second request often
failed, so tiles showed but were never cached and re-zooming re-fetched them.
Now one fetch caches and displays from the same blob, so every shown tile is
cached. CSP already allows connect-src to the OSM tile host.
Verified on device: container bg rgb(27,31,36) under dark map; IndexedDB tile
cache populated.