Skip to content

Add code to use multiple tile formats and providers #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ea979f5
Add code to use multiple tile formats and providers
CelliesProjects Jun 4, 2025
b64bdfe
Refactor TileProvider setup
CelliesProjects Jun 4, 2025
ccd0dfd
Add example declaration for multiple tile providers
CelliesProjects Jun 4, 2025
41fa98f
Cleanup
CelliesProjects Jun 4, 2025
03e0d19
Cleanup
CelliesProjects Jun 4, 2025
c884883
Combine if statements
CelliesProjects Jun 4, 2025
249fd5a
Update comment
CelliesProjects Jun 4, 2025
484c915
Cleanup
CelliesProjects Jun 4, 2025
2b75214
Fix providers
CelliesProjects Jun 4, 2025
9bff9fc
Add a modded DejaVu9 font with a percent sign ©
CelliesProjects Jun 4, 2025
fa14dea
Cleanup
CelliesProjects Jun 5, 2025
30b9320
Update README
CelliesProjects Jun 5, 2025
d58facf
Cleanup
CelliesProjects Jun 5, 2025
2654ae8
Add Thunderforest TransportDark as a provider
CelliesProjects Jun 5, 2025
c22bafc
Update README.md
CelliesProjects Jun 5, 2025
73c5cad
Use taskYIELD() to get better utilisation when downloading
CelliesProjects Jun 5, 2025
f985515
Fetch task priority to 1
CelliesProjects Jun 5, 2025
d4d4122
Fix tile provider comments
CelliesProjects Jun 5, 2025
0004d3d
Update README
CelliesProjects Jun 5, 2025
0849add
Use RAII to clean up `tilesCache`
CelliesProjects Jun 5, 2025
df0d107
Update README
CelliesProjects Jun 5, 2025
f00422f
Fix codacy issue
CelliesProjects Jun 5, 2025
3450db0
Add provider getters
CelliesProjects Jun 6, 2025
822485b
Cleanup
CelliesProjects Jun 6, 2025
1669e51
Change `tilesToCover` to `tilesNeeded`
CelliesProjects Jun 6, 2025
dd4e3a1
Cleanup
CelliesProjects Jun 6, 2025
de9c091
Cleanup
CelliesProjects Jun 6, 2025
26fd2b4
Small fix
CelliesProjects Jun 6, 2025
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
84 changes: 72 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A composed map can be pushed to the screen, saved to SD or used for further comp
Downloaded tiles are cached in psram for reuse.

This library should work on any ESP32 type with psram and a LovyanGFX compatible display.
OSM tiles are quite large -128kB per tile- so psram is required.
OSM tiles are quite large at 128kB or insane large at 512kB per tile, so psram is required.

This project is not endorsed by or affiliated with the OpenStreetMap Foundation.
Use of any OSMF provided service is governed by the [OSMF Terms of Use](https://osmfoundation.org/wiki/Terms_of_Use).
Expand All @@ -40,43 +40,103 @@ framework = arduino
lib_deps =
celliesprojects/OpenStreetMap-esp32@^1.0.6
lovyan03/LovyanGFX@^1.2.7
https://github.com/bitbank2/PNGdec@^1.1.3
bitbank2/PNGdec@^1.1.3
```

## Functions

### Set map size

```c++
void setSize(uint16_t w, uint16_t h);
void setSize(uint16_t w, uint16_t h)
```

- If no size is set a 320px by 240px map will be returned by `fetchMap`.
- If no size is set a 320px by 240px map will be returned.
- The tile cache should be freed with `freeTilesCache()` after setting a new bigger map size.

### Resize cache
### Get the number of tiles needed to cache a map

```c++
bool resizeTilesCache(uint16_t numberOfTiles);
uint16_t tilesNeeded(uint16_t w, uint16_t h)
```

- If the cache is not resized before the first call to `fetchMap`, it will auto initialize with space for 10 tiles on the first call.
- Each tile allocates 128 kB psram.
- The cache content is cleared before resizing.
This returns the number of tiles required to cache the given map size.

### Free the memory used by the tile cache
### Resize the tiles cache

```c++
void freeTilesCache();
bool resizeTilesCache(uint16_t numberOfTiles)
```

- If the cache is not resized before the first call to `fetchMap`, the cache will be auto initialized.
- The cache content is cleared before resizing.
- Each 256px tile allocates **128kB** psram.
- Each 512px tile allocates **512kB** psram.

**Don't over-allocate the cache**
When resizing the cache, keep in mind that the map sprite also uses psram.
The PNG decoders -~50kB for each core- also live in psram.
Use the above `tilesNeeded` function to calculate a safe and sane cache size.

### Fetch a map

```c++
bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom);
bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom)
```

- Overflowing `longitude` are wrapped and normalized to +-180°.
- Overflowing `latitude` are clamped to +-90°.
- Valid range for the `zoom` level is 1-18.

### Free the memory used by the tile cache

```c++
void freeTilesCache()
```

### Switch to a different tile provider

```c++
bool setTileProvider(int index)
```

This function will switch to a tile provider (if) that is user defined in `src/TileProvider.hpp`.

- Returns `true` and clears the cache on success.
- Returns `false` -and the current tile provider is unchanged- if no provider at the index is defined.

### Get the number of defined providers

`OSM_TILEPROVIDERS` gives the number of defined providers.

Example use:

```c++
const int numberOfProviders = OSM_TILEPROVIDERS;
```

In the default setup there is only one provider defined.
See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require an API key and commenting/uncommenting 2 lines.
Registration and a hobby tier are available for free.

### Get the provider name

```c++
char *getProviderName()
```

### Get the minimum zoom level

```c++
int getMinZoom()
```

### Get the maximum zoom level

```c++
int getMaxZoom()
```

## Example code

### Example returning the default 320x240 map
Expand Down
4 changes: 2 additions & 2 deletions src/CachedTile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ struct CachedTile
free();
}

bool allocate()
bool allocate(int tileSize)
{
buffer = static_cast<uint16_t *>(heap_caps_malloc(256 * 256 * sizeof(uint16_t), MALLOC_CAP_SPIRAM));
buffer = static_cast<uint16_t *>(heap_caps_malloc(tileSize * tileSize * sizeof(uint16_t), MALLOC_CAP_SPIRAM));
return buffer != nullptr;
}

Expand Down
99 changes: 57 additions & 42 deletions src/OpenStreetMap-esp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,26 @@ void OpenStreetMap::computeRequiredTiles(double longitude, double latitude, uint
const int32_t targetTileY = static_cast<int32_t>(exactTileY);

// Compute the offset inside the tile for the given coordinates
const int16_t targetOffsetX = (exactTileX - targetTileX) * OSM_TILESIZE;
const int16_t targetOffsetY = (exactTileY - targetTileY) * OSM_TILESIZE;
const int16_t targetOffsetX = (exactTileX - targetTileX) * currentProvider->tileSize;
const int16_t targetOffsetY = (exactTileY - targetTileY) * currentProvider->tileSize;

// Compute the offset for tiles covering the map area to keep the location centered
const int16_t tilesOffsetX = mapWidth / 2 - targetOffsetX;
const int16_t tilesOffsetY = mapHeight / 2 - targetOffsetY;

// Compute number of colums required
const float colsLeft = 1.0 * tilesOffsetX / OSM_TILESIZE;
const float colsRight = float(mapWidth - (tilesOffsetX + OSM_TILESIZE)) / OSM_TILESIZE;
const float colsLeft = 1.0 * tilesOffsetX / currentProvider->tileSize;
const float colsRight = float(mapWidth - (tilesOffsetX + currentProvider->tileSize)) / currentProvider->tileSize;
numberOfColums = ceil(colsLeft) + 1 + ceil(colsRight);

startOffsetX = tilesOffsetX - (ceil(colsLeft) * OSM_TILESIZE);
startOffsetX = tilesOffsetX - (ceil(colsLeft) * currentProvider->tileSize);

// Compute number of rows required
const float rowsTop = 1.0 * tilesOffsetY / OSM_TILESIZE;
const float rowsBottom = float(mapHeight - (tilesOffsetY + OSM_TILESIZE)) / OSM_TILESIZE;
const float rowsTop = 1.0 * tilesOffsetY / currentProvider->tileSize;
const float rowsBottom = float(mapHeight - (tilesOffsetY + currentProvider->tileSize)) / currentProvider->tileSize;
const uint32_t numberOfRows = ceil(rowsTop) + 1 + ceil(rowsBottom);

startOffsetY = tilesOffsetY - (ceil(rowsTop) * OSM_TILESIZE);
startOffsetY = tilesOffsetY - (ceil(rowsTop) * currentProvider->tileSize);

log_v(" Need %i * %i tiles. First tile offset is %d,%d",
numberOfColums, numberOfRows, startOffsetX, startOffsetY);
Expand Down Expand Up @@ -171,17 +171,11 @@ bool OpenStreetMap::isTileCachedOrBusy(uint32_t x, uint32_t y, uint8_t z)

void OpenStreetMap::freeTilesCache()
{
for (auto &tile : tilesCache)
tile.free();

tilesCache.clear();
std::vector<CachedTile>().swap(tilesCache);
}

bool OpenStreetMap::resizeTilesCache(uint16_t numberOfTiles)
{
if (tilesCache.size() == numberOfTiles)
return true;

if (!numberOfTiles)
{
log_e("Invalid cache size: %d", numberOfTiles);
Expand All @@ -193,7 +187,7 @@ bool OpenStreetMap::resizeTilesCache(uint16_t numberOfTiles)

for (auto &tile : tilesCache)
{
if (!tile.allocate())
if (!tile.allocate(currentProvider->tileSize))
{
log_e("Tile cache allocation failed!");
freeTilesCache();
Expand All @@ -211,7 +205,7 @@ void OpenStreetMap::updateCache(const tileList &requiredTiles, uint8_t zoom)
if (!jobs.empty())
{
runJobs(jobs);
log_i("Updated %i tiles in %lu ms - %i ms/tile", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size());
log_d("Updated %i tiles in %lu ms - %i ms/tile", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size());
}
}

Expand Down Expand Up @@ -270,8 +264,8 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, const tileList &requiredT
continue;
}

int drawX = startOffsetX + (tileIndex % numberOfColums) * OSM_TILESIZE;
int drawY = startOffsetY + (tileIndex / numberOfColums) * OSM_TILESIZE;
int drawX = startOffsetX + (tileIndex % numberOfColums) * currentProvider->tileSize;
int drawY = startOffsetY + (tileIndex / numberOfColums) * currentProvider->tileSize;

auto it = std::find_if(tilesCache.begin(), tilesCache.end(),
[&](const CachedTile &tile)
Expand All @@ -280,7 +274,7 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, const tileList &requiredT
});

if (it != tilesCache.end())
mapSprite.pushImage(drawX, drawY, OSM_TILESIZE, OSM_TILESIZE, it->buffer);
mapSprite.pushImage(drawX, drawY, currentProvider->tileSize, currentProvider->tileSize, it->buffer);
else
log_w("Tile (z=%d, x=%d, y=%d) not found in cache", zoom, tileX, tileY);

Expand All @@ -293,8 +287,8 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, const tileList &requiredT
mapSprite.setTextColor(TFT_WHITE, TFT_BLACK);
else
mapSprite.setTextColor(TFT_BLACK);
mapSprite.drawRightString(" Map data from OpenStreetMap.org ",
mapSprite.width(), mapSprite.height() - 10, &DejaVu9);
mapSprite.drawRightString(currentProvider->attribution,
mapSprite.width(), mapSprite.height() - 10, &DejaVu9Modded);
mapSprite.setTextColor(TFT_WHITE, TFT_BLACK);

return true;
Expand All @@ -308,7 +302,7 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la
return false;
}

if (!zoom || zoom > OSM_MAX_ZOOM)
if (zoom < currentProvider->minZoom || zoom > currentProvider->maxZoom)
{
log_e("Invalid zoom level: %d", zoom);
return false;
Expand All @@ -320,14 +314,10 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la
return false;
}

if (!tilesCache.capacity())
if (!tilesCache.capacity() && !resizeTilesCache(tilesNeeded(mapWidth, mapHeight)))
{
log_w("Cache not initialized, setting up a default cache...");
if (!resizeTilesCache(OSM_DEFAULT_CACHE_ITEMS))
{
log_e("Could not allocate tile cache");
return false;
}
log_e("Could not allocate tile cache");
return false;
}

longitude = fmod(longitude + 180.0, 360.0) - 180.0;
Expand Down Expand Up @@ -366,7 +356,7 @@ bool OpenStreetMap::fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t
result = "Timeout: " + String(OSM_TILE_TIMEOUT_MS) + " ms";
return false;
}
vTaskDelay(pdMS_TO_TICKS(1));
taskYIELD();
continue;
}

Expand All @@ -382,7 +372,7 @@ bool OpenStreetMap::fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t
lastReadTime = millis();
}
else
vTaskDelay(pdMS_TO_TICKS(1));
taskYIELD();
}
return true;
}
Expand Down Expand Up @@ -432,22 +422,25 @@ std::optional<std::unique_ptr<MemoryBuffer>> OpenStreetMap::urlToBuffer(const ch

void OpenStreetMap::PNGDraw(PNGDRAW *pDraw)
{
uint16_t *destRow = currentInstance->currentTileBuffer + (pDraw->y * OSM_TILESIZE);
uint16_t *destRow = currentInstance->currentTileBuffer + (pDraw->y * currentInstance->currentProvider->tileSize);
getPNGCurrentCore()->getLineAsRGB565(pDraw, destRow, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
}

bool OpenStreetMap::fetchTile(CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result)
{
char url[64];
snprintf(url, sizeof(url), "https://tile.openstreetmap.org/%u/%u/%u.png",
static_cast<unsigned int>(zoom),
static_cast<unsigned int>(x),
static_cast<unsigned int>(y));

const auto buffer = urlToBuffer(url, result);
String url = currentProvider->urlTemplate;
url.replace("{x}", String(x));
url.replace("{y}", String(y));
url.replace("{z}", String(zoom));
if (currentProvider->requiresApiKey && strstr(url.c_str(), "{apiKey}"))
url.replace("{apiKey}", currentProvider->apiKey);

const auto buffer = urlToBuffer(url.c_str(), result);
if (!buffer)
return false;

url.clear();

PNG *png = getPNGCurrentCore();
const int16_t rc = png->openRAM(buffer.value()->get(), buffer.value()->size(), PNGDraw);
if (rc != PNG_SUCCESS)
Expand All @@ -456,7 +449,7 @@ bool OpenStreetMap::fetchTile(CachedTile &tile, uint32_t x, uint32_t y, uint8_t
return false;
}

if (png->getWidth() != OSM_TILESIZE || png->getHeight() != OSM_TILESIZE)
if (png->getWidth() != currentProvider->tileSize || png->getHeight() != currentProvider->tileSize)
{
result = "Unexpected tile size: w=" + String(png->getWidth()) + " h=" + String(png->getHeight());
return false;
Expand All @@ -468,7 +461,7 @@ bool OpenStreetMap::fetchTile(CachedTile &tile, uint32_t x, uint32_t y, uint8_t
const int decodeResult = png->decode(0, PNG_FAST_PALETTE);
if (decodeResult != PNG_SUCCESS)
{
result = "Decoding " + String(url) + " failed with code: " + String(decodeResult);
result = "Decoding " + url + " failed with code: " + String(decodeResult);
tile.valid = false;
return false;
}
Expand Down Expand Up @@ -550,3 +543,25 @@ bool OpenStreetMap::startTileWorkerTasks()
log_i("Started %d tile worker task(s)", numberOfWorkers);
return true;
}

uint16_t OpenStreetMap::tilesNeeded(uint16_t mapWidth, uint16_t mapHeight)
{
const int tileSize = currentProvider->tileSize;
int tilesX = (mapWidth + tileSize - 1) / tileSize + 1;
int tilesY = (mapHeight + tileSize - 1) / tileSize + 1;
return tilesX * tilesY;
}

bool OpenStreetMap::setTileProvider(int index)
{
if (index < 0 || index >= OSM_TILEPROVIDERS)
{
log_e("invalid provider index");
return false;
}

currentProvider = &tileProviders[index];
freeTilesCache();
log_i("provider changed to '%s'", currentProvider->name);
return true;
}
Loading