diff --git a/Apps/Maps/CHANGELOG.md b/Apps/Maps/CHANGELOG.md index 0abef58..11416a8 100644 --- a/Apps/Maps/CHANGELOG.md +++ b/Apps/Maps/CHANGELOG.md @@ -1,5 +1,99 @@ # Changelog +## 0.8.1 + +**Menu** +- The main menu is a grid of nine tiles with icons: Search, Download, Maps, Waypoints, Track, GPX, Location, + Settings, About. W A S D or the arrows move in both directions, Enter opens, Esc closes; the trackball moves tile by + tile or points; tap a tile. The menu opens on the tile used last. +- A strip above the tiles shows what is running, only while it runs: the download with its progress, and Show route + or Stop and Clear route while there is a route. +- Location switches on and off in place and shows its state on the tile. Exit is a small button at the top right. +- Waypoints always opens the list; its first row is "Add waypoint here" (P and a long press on the map still add one). + +**Following a GPX track** +- Menu, GPX, a file, "Follow this track": the first track or route of the file becomes the route (its card shows + the length and walking time; Start follows it with the bar, the remaining distance and the arrival). No turns are + announced, and away from the track the bar says "Off the track" with the distance instead of searching another + way. Tracks of any length (long ones are thinned to 6,000 points). Choosing a file now asks: show or hide it on + the map, or follow it. + +**Off the route** +- A new route comes much faster on a long route: the app looks for the way back to the route a little ahead and + keeps the rest of it, instead of searching all the way to the destination again (which took seconds, by which + time a bike or a bus was somewhere else, so the new route was left at once and the bar never stopped + "recalculating"). +- A new route is asked for at once every time the route is left, from the position itself (the 10 seconds between + two new routes are gone). Only after a search that found no way does the next one wait 5 seconds; the bar then + shows how far the route is and that there is no new route yet. +- Long car routes first look along the larger roads between their two ends, as a driver would: more routes + across a city are found (49 of 50 test pairs on the T-Deck, 48 before) and they come a little faster. +- While navigating, what the app saw (positions, distance from the route, new routes) is written to `navlog.txt` + in the app's folder, to find out afterwards what happened on a trip. + +**Faster** +- Names and icons appear sooner after the map stops moving: placing them takes a quarter to almost half less + time on the T-Deck. + +**Long routes** +- Routes between towns and across a country: near the start and the destination the app uses the streets of the + detailed map, between them the main roads of the zoomed-out map (zoom 10). The detailed map is only needed around + both ends; for the way between, download the wider area once while zoomed out. On the T-Deck a 27 km route + between two towns with no detailed map between them is found in 2 to 8 seconds. +- A new route in the middle of such a drive starts on the main road you are on, also where no detailed map is + on the card. +- On the main roads the line is the zoomed-out map's, a little coarser than the road on screen; the app allows + for that before it says you left the route. +- A map that was just downloaded can be routed on at once: the search prepares the tiles it needs itself + (before, the route was "not found" until the app had prepared them in idle time). + +**Search** +- Exact names are ordered by what they are before how near they are: a town or village first, then stations, + airports, hospitals, malls, museums and the like, then other places, and bus stops, information boards and such + last (a bus stop named like the station no longer comes before the station). +- Streets that only have an international name in the map data are found (Athens: 893 of 9,542 street lines). + The search indexes on the card are made again in idle time for this. + +**Controls and screens** +- A search button among the map's controls, under the menu button (key /). +- Settings: one line under the list says what the highlighted setting does. +- With a keyboard, the waypoint sheet, the route card and the download card show their keys in one small line. +- On boards without a keyboard, list rows and buttons are at least 36 px high. +- The same margins, corner radii and dimming on every sheet, card and dialog; Backspace in the search field always + deletes a letter and only closes when the field is empty. + +**Labels and POIs** +- Labels no longer jump when the map is moved: their places are fixed to the map, a label at the screen's edge is cut + instead of dropped, the same view always gives the same labels, and small labels (streets, POIs, house numbers) that + are on screen stay while the map moves or zooms. +- More POIs: each takes only the room of its icon (the box around it was more than twice as wide as the icon), and + all icons are placed before any name. A POI's name shows from zoom 17 where there is room next to its icon; select + the icon to read it otherwise. Bratislava centre at zoom 17: 61 POIs on screen (11 before), all of them at zoom 19. +- Zoomed out, cities show by importance: the capitals and largest cities first, more with every zoom level, the same + rule everywhere (Krakow no longer shows where Wien and Budapest do not). Country names are smaller from zoom 5 so + capitals fit between them; a city or country name moves a little above, below or beside its point when its place + is taken; regions (Burgenland) come after cities. + +**Fixes** +- Recorded tracks: a write that the card cut short no longer leaves a broken point followed by the same points + again; closing an unfinished track after a crash never leaves the card without the track. +- Region information, settings, the last view and the waypoints are replaced safely: a failed write keeps the old + file, and a power cut between the two steps is repaired at the next start. +- Deleting a region also removes its search index (the folder used to stay behind). +- Damaged map files end with an error instead of hanging or overrunning memory: tiles whose compressed data never + ends, wrong sizes in prepared tiles and label files, broken geometry commands, label points far outside the tile. +- The search index build and the preparation of tiles in the background no longer run at the same time (the two + together left 67 KB less memory free). +- Navigation: a weak GPS signal far from the route ends the "off route" state instead of asking for a new route + again and again; running out of memory during a route search says so instead of "no route". +- Downloaded maps: an action chosen while the list changes (a download finishing) acts on the region that was chosen. +- Recorded tracks are listed newest first; a failed settings write puts the setting back; "No tracks yet" and a + catalog that cannot be read no longer change where About returns to. +- GPX: names keep whole letters when cut, control characters are left out, empty names are not written with every + track point, "inf" and "nan" are not read as elevations. +- Search: a house number of 65535 no longer matches addresses without a number; a very long word is one word. +- The Enter walk over POIs takes the nearest ones first when more than 16 are on screen. + ## 0.5.0 Offline search of places, streets, addresses and kinds of places in the downloaded maps. diff --git a/Apps/Maps/README.md b/Apps/Maps/README.md index 64bf64e..f471776 100644 --- a/Apps/Maps/README.md +++ b/Apps/Maps/README.md @@ -15,7 +15,7 @@ for downloading maps. Tested on Tactility 0.8.0-dev (main, November 2026) on a L 1. Install "Maps" from Tactility's App Hub. Or copy the `.app` file to the SD card, open it in the Files app and confirm "Install". 2. Start "Maps" from the app list. The first time it opens at ESA ESTEC in Noordwijk with no map data yet: move - to your area, open the menu (M or ☰) and choose Download map. + to your area, open the menu (M or ☰) and choose Download. ## How it works @@ -51,7 +51,7 @@ circle), the scale bar, and the © mark. | Add a waypoint | P | — | Hold ⌖ (your position) or hold the map (that point) | | A place or house number | Show the controls, then Enter (nearest the crosshair; Enter again: next) | Click it | Tap it | | Menu | M | Long click | ☰ button | -| Download an area | Shift + D | — | Menu → Download map | +| Download an area | Shift + D | — | Menu → Download | | Leave the download frame | Esc | — | The round < in its top left corner | | Type a name (waypoint, rename) | Keyboard | — | On-screen keyboard | | Credits | — | Click © | Tap ©, tap again for About and licences | @@ -176,9 +176,14 @@ takes about 5 minutes on a T-Deck; it is made again when the region's tiles are z14 tiles get one index per block of 15 x 15 tiles. Until a region's index is ready, search doesn't find anything in it. The PC tool `host/search_build --tiles` makes the same file. -**Menu:** Download map, Downloaded maps, Waypoints, Add waypoint, Record track, GPX files, Location, Settings, About and -licences, Exit. W/S, trackball or finger to move; Enter, click or tap to choose; Esc, Backspace or tap -outside to close. +**Menu (0.6.0):** a grid of nine tiles: Search, Download (choose an area on the map or a region from the list), Maps (the +downloaded maps), Waypoints (the list; its first row adds one at the map's centre), Track (record, pause, stop, +recorded tracks), GPX (files to show), Location (switches on and off in place; the tile says On or Off), Settings, +About. W A S D, the arrows or the trackball move from tile to tile, Enter or a click opens, a tap opens directly; +Esc, Backspace, M, a swipe to the right or the arrow at the top left close it. The menu opens on the tile you used +last. While a download runs or a route exists, a strip above the tiles shows it: the download with its progress +(opens the download card), Show route or Stop, and Clear route. Exit is the small button at the top right. +With the controls shown there is also a search button under the menu button (key /). **Lists and actions.** Enter or a tap on a row opens its actions; lists have a back arrow and a check mark in their header; swipe right or the back arrow goes back, and dragging a list scrolls it. Lists open @@ -227,7 +232,7 @@ not updated. Tactility's location service on or off, with a message. If it's off, the ⌖ button is crossed out and pressing it says how to enable it in Tactility's settings. -**Waypoints:** P, or holding ⌖, saves a waypoint at your position; Menu → Add waypoint saves one at the +**Waypoints:** P, or holding ⌖, saves a waypoint at your position; Menu → Waypoints → Add waypoint here saves one at the map centre; on touch screens holding the map saves one where you hold. A sheet asks for the name of a new waypoint; typing replaces the suggested name. Waypoints are named WP 001, WP 002, …; they show as pins at every zoom level, all the same size. Tap a pin for its card: name, coordinates and distance and @@ -235,7 +240,7 @@ direction from you. Esc closes the card. Menu → Waypoints lists them by distan there, R renames (keyboard), Del or hold deletes after asking. They are kept in `waypoints.gpx` (GPX 1.1) in the app's folder on the card. -**Track recording:** Menu → Record track → Start recording. A red ● with the time shows top right (with +**Track recording:** Menu → Track → Start recording. A red ● with the time shows top right (with the controls shown also the distance). The same menu entry pauses, resumes and stops. Points are taken from the position when you moved more than 5 m (or more than the GPS accuracy), or every 30 s while moving; approximate positions (from the network) are skipped. The track is drawn on the map as a red diff --git a/Apps/Maps/main/Source/app/BinFontText.cpp b/Apps/Maps/main/Source/app/BinFontText.cpp index 9e7a0db..2ec1710 100644 --- a/Apps/Maps/main/Source/app/BinFontText.cpp +++ b/Apps/Maps/main/Source/app/BinFontText.cpp @@ -123,6 +123,7 @@ void BinFontText::close() { } font = nullptr; owned = false; + dropGlyphs(); } namespace { @@ -220,22 +221,85 @@ void BinFontText::drawText(Canvas& canvas, int x, int y, const char* text, uint1 binfont_draw_text_rgb565(target, x, y + ascentPixels, font, withFallback(text), color, PIXEL_BUFFER_CONVERSION_AVERAGE); } +void BinFontText::dropGlyphs() { + if (glyphs != nullptr) { + largeFree(glyphs); + largeFree(glyphMasks); + } + glyphs = nullptr; + glyphMasks = nullptr; + glyphMasksUsed = 0; +} + bool BinFontText::glyph(uint32_t codepoint, LabelGlyph& out, bool withMask) { - BinFontGlyph found; - if (font == nullptr || !binfont_get_glyph(font, codepoint, &found)) { + if (font == nullptr) { return false; } - out.id = found.glyph_id; - out.advanceX16 = static_cast(found.advance_x16); - out.left = found.x; - // binfont gives the box's bottom above the baseline, labels want its top below it - out.top = -(found.y + found.height); - out.width = found.width; - out.height = found.height; + if (glyphs == nullptr) { + // Without memory for the cache every glyph is looked up as before + glyphs = static_cast(largeAlloc(sizeof(CachedGlyph) * GLYPH_SLOTS)); + glyphMasks = glyphs != nullptr ? static_cast(largeAlloc(GLYPH_MASK_BYTES)) : nullptr; + if (glyphMasks == nullptr) { + largeFree(glyphs); + glyphs = nullptr; + } else { + memset(glyphs, 0, sizeof(CachedGlyph) * GLYPH_SLOTS); + glyphMasksUsed = 0; + } + } + CachedGlyph* cached = glyphs != nullptr ? &glyphs[(codepoint * 2654435761u) >> 24] : nullptr; + BinFontGlyph found {}; + bool looked = false; + if (cached == nullptr || !cached->used || cached->codepoint != codepoint) { + if (!binfont_get_glyph(font, codepoint, &found)) { + return false; + } + looked = true; + out.id = found.glyph_id; + out.advanceX16 = static_cast(found.advance_x16); + out.left = found.x; + // binfont gives the box's bottom above the baseline, labels want its top below it + out.top = -(found.y + found.height); + out.width = found.width; + out.height = found.height; + if (cached != nullptr) { + *cached = { codepoint, static_cast(out.id), static_cast(out.advanceX16), NO_MASK, static_cast(out.left), + static_cast(out.top), static_cast(out.width), static_cast(out.height), true }; + } + } else { + out.id = cached->id; + out.advanceX16 = cached->advanceX16; + out.left = cached->left; + out.top = cached->top; + out.width = cached->width; + out.height = cached->height; + } out.alpha = nullptr; - if (withMask && found.width > 0 && found.height > 0 && found.width * found.height <= MASK_BYTES && - binfont_get_glyph_bitmap(font, &found, mask, found.width) == ERROR_NONE) { - out.alpha = mask; + const uint32_t size = static_cast(out.width) * static_cast(out.height); + if (!withMask || size == 0 || size > MASK_BYTES) { + return true; + } + if (cached != nullptr && cached->maskOffset != NO_MASK) { + out.alpha = glyphMasks + cached->maskOffset; + return true; + } + if (!looked && !binfont_get_glyph(font, codepoint, &found)) { + return true; + } + if (cached != nullptr && glyphMasksUsed + size > GLYPH_MASK_BYTES) { + // Full (a view with several scripts): started again, this glyph is its first + const CachedGlyph keep = *cached; + memset(glyphs, 0, sizeof(CachedGlyph) * GLYPH_SLOTS); + glyphMasksUsed = 0; + *cached = keep; + } + uint8_t* into = cached != nullptr ? glyphMasks + glyphMasksUsed : mask; + if (binfont_get_glyph_bitmap(font, &found, into, found.width) == ERROR_NONE) { + out.alpha = into; + if (cached != nullptr) { + cached->maskOffset = glyphMasksUsed; + glyphMasksUsed += size; + } } return true; } diff --git a/Apps/Maps/main/Source/app/BinFontText.h b/Apps/Maps/main/Source/app/BinFontText.h index 290b483..e850fa4 100644 --- a/Apps/Maps/main/Source/app/BinFontText.h +++ b/Apps/Maps/main/Source/app/BinFontText.h @@ -56,6 +56,30 @@ class BinFontText final : public Font, public LabelFont { /** The mask of the last glyph asked for with withMask; labels use fonts of at most about 30 px. */ static constexpr int MASK_BYTES = 48 * 48; uint8_t mask[MASK_BYTES] {}; + /** + * The glyphs asked for before, with their masks: looking a glyph up and unpacking its bitmap in the binfont-module + * took about 2 ms per label, most of the time of placing the labels of a view (15 Nov). Direct mapped by + * codepoint; the masks in one block of PSRAM that is started again when it is full. + */ + struct CachedGlyph { + uint32_t codepoint; + uint32_t id; + int32_t advanceX16; + /** Into glyphMasks; NO_MASK when the mask was not asked for yet (or is too large). */ + uint32_t maskOffset; + int16_t left; + int16_t top; + uint16_t width; + uint16_t height; + bool used; + }; + static constexpr int GLYPH_SLOTS = 256; + static constexpr uint32_t GLYPH_MASK_BYTES = 24 * 1024; + static constexpr uint32_t NO_MASK = UINT32_MAX; + void dropGlyphs(); + CachedGlyph* glyphs = nullptr; + uint8_t* glyphMasks = nullptr; + uint32_t glyphMasksUsed = 0; /** The canvas the text was last drawn on, wrapped once as a PixelBuffer. */ PixelBuffer* target = nullptr; uint16_t* targetPixels = nullptr; diff --git a/Apps/Maps/main/Source/app/BrowsedCounter.cpp b/Apps/Maps/main/Source/app/BrowsedCounter.cpp index 4366318..250ba45 100644 --- a/Apps/Maps/main/Source/app/BrowsedCounter.cpp +++ b/Apps/Maps/main/Source/app/BrowsedCounter.cpp @@ -41,6 +41,7 @@ bool countJob(IoTask& io, void* context) { auto* job = static_cast(context); job->count = 0; job->complete = false; + bool truncated = false; for (int z = job->plan.minZoom; z <= job->plan.maxZoom; z++) { const TileRange range = tileRange(job->plan.box, z); char folder[220]; @@ -64,7 +65,8 @@ bool countJob(IoTask& io, void* context) { } } closedir(columns); - (void)more; + // Columns beyond the list are not counted, so the result must not pass as complete + truncated = truncated || more; for (int i = 0; i < insideCount; i++) { // Tiles on screen come first: give the card back and say the count is not complete if (io.shouldYield()) { @@ -85,6 +87,10 @@ bool countJob(IoTask& io, void* context) { closedir(files); } } + // Still complete: an incomplete count is counted again without end. The number is then too low; say so in the log. + if (truncated) { + logError("Browsed tiles: more than 96 columns in a zoom level, the count is too low"); + } job->complete = true; return true; } diff --git a/Apps/Maps/main/Source/app/Debug.cpp b/Apps/Maps/main/Source/app/Debug.cpp index f7c5ab2..43831cb 100644 --- a/Apps/Maps/main/Source/app/Debug.cpp +++ b/Apps/Maps/main/Source/app/Debug.cpp @@ -145,6 +145,30 @@ DebugScript::Step DebugScript::next(uint32_t now) { step.action = Action::TouchOnly; return step; } + if (strcmp(command, "pointer") == 0) { + // "pointer ": the trackball pointer is shown there; "key c" then clicks at it + step.action = Action::Pointer; + step.values[0] = step.values[1] = 0.0f; + sscanf(argument, "%f %f", &step.values[0], &step.values[1]); + return step; + } + if (strcmp(command, "followgpx") == 0) { + // "followgpx ": follows the track of a GPX file in the app's folder, as Menu, GPX, Follow this track + step.action = Action::FollowGpx; + snprintf(step.name, sizeof(step.name), "%s", argument); + return step; + } + if (strcmp(command, "restart") == 0) { + // "restart": the board restarts (a failed app load leaves Tactility with less memory until it does) + step.action = Action::Restart; + return step; + } + if (strcmp(command, "memory") == 0) { + // "memory