Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions Apps/Maps/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
19 changes: 12 additions & 7 deletions Apps/Maps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -227,15 +232,15 @@ 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
direction from you. Esc closes the card. Menu → Waypoints lists them by distance: Enter or tap goes
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
Expand Down
88 changes: 76 additions & 12 deletions Apps/Maps/main/Source/app/BinFontText.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ void BinFontText::close() {
}
font = nullptr;
owned = false;
dropGlyphs();
}

namespace {
Expand Down Expand Up @@ -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<int>(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<CachedGlyph*>(largeAlloc(sizeof(CachedGlyph) * GLYPH_SLOTS));
glyphMasks = glyphs != nullptr ? static_cast<uint8_t*>(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<int>(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<uint32_t>(out.id), static_cast<int32_t>(out.advanceX16), NO_MASK, static_cast<int16_t>(out.left),
static_cast<int16_t>(out.top), static_cast<uint16_t>(out.width), static_cast<uint16_t>(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<uint32_t>(out.width) * static_cast<uint32_t>(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;
}
Expand Down
24 changes: 24 additions & 0 deletions Apps/Maps/main/Source/app/BinFontText.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion Apps/Maps/main/Source/app/BrowsedCounter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ bool countJob(IoTask& io, void* context) {
auto* job = static_cast<CountJob*>(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];
Expand All @@ -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()) {
Expand All @@ -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;
}
Expand Down
Loading
Loading