├── LICENSE ├── examples └── osm-basic-example │ └── osm-basic-example.ino ├── library.json ├── src ├── TileJob.hpp ├── MemoryBuffer.hpp ├── MemoryBuffer.cpp ├── CachedTile.hpp ├── ReusableTileFetcher.hpp ├── TileProvider.hpp ├── OpenStreetMap-esp32.hpp ├── fonts │ └── DejaVu9-modded.h ├── ReusableTileFetcher.cpp └── OpenStreetMap-esp32.cpp └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cellie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/osm-basic-example/osm-basic-example.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define LGFX_M5STACK_CORE2 // for supported devices see 5 | // https://github.com/lovyan03/LovyanGFX 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | const char *ssid = "xxx"; 13 | const char *password = "xxx"; 14 | 15 | LGFX display; 16 | OpenStreetMap osm; 17 | 18 | double longitude = 5.9; 19 | double latitude = 51.5; 20 | int zoom = 5; 21 | 22 | void setup() 23 | { 24 | Serial.begin(115200); 25 | Serial.printf("WiFi connecting to %s\n", ssid); 26 | 27 | WiFi.begin(ssid, password); 28 | while (WiFi.status() != WL_CONNECTED) 29 | { 30 | delay(10); 31 | Serial.print("."); 32 | } 33 | 34 | Serial.println("\nWiFi connected"); 35 | 36 | display.begin(); 37 | display.setRotation(1); 38 | display.setBrightness(110); 39 | 40 | // create a sprite to store the map 41 | LGFX_Sprite map(&display); 42 | 43 | // returned map is 320px by 240px by default 44 | const bool success = osm.fetchMap(map, longitude, latitude, zoom); 45 | 46 | if (success) 47 | map.pushSprite(0, 0); 48 | else 49 | Serial.println("Failed to fetch map."); 50 | } 51 | 52 | void loop() 53 | { 54 | delay(1000); 55 | } 56 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenStreetMap-esp32", 3 | "version": "1.2.2", 4 | "description": "This PlatformIO library provides a OpenStreetMap (OSM) map fetching and caching system for ESP32-based devices.", 5 | "keywords": [ 6 | "esp32", 7 | "gps", 8 | "location", 9 | "geolocation", 10 | "tracking", 11 | "map", 12 | "tiles", 13 | "tile-caching", 14 | "openstreetmap", 15 | "osm", 16 | "lovyangfx", 17 | "pngdec" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/CelliesProjects/OpenStreetMap-esp32" 22 | }, 23 | "authors": [ 24 | { 25 | "name": "CelliesProjects" 26 | } 27 | ], 28 | "frameworks": [ 29 | "arduino" 30 | ], 31 | "platforms": [ 32 | "espressif32" 33 | ], 34 | "dependencies": [ 35 | { 36 | "name": "LovyanGFX", 37 | "version": "https://github.com/lovyan03/LovyanGFX.git#1.2.7" 38 | }, 39 | { 40 | "name": "PNGdec", 41 | "version": "https://github.com/bitbank2/PNGdec.git#1.1.3" 42 | } 43 | ], 44 | "build": { 45 | "flags": [ 46 | "-std=gnu++17" 47 | ] 48 | }, 49 | "license": "MIT", 50 | "include": [ 51 | "src/OpenStreetMap-esp32.hpp" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/TileJob.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #ifndef TILEJOB_HPP_ 25 | #define TILEJOB_HPP_ 26 | 27 | #include "CachedTile.hpp" 28 | 29 | struct TileJob 30 | { 31 | uint32_t x; 32 | uint32_t y; 33 | uint8_t z; 34 | CachedTile *tile; 35 | }; 36 | 37 | static_assert(sizeof(TileJob) >= 0, "Suppress unusedStruct"); 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /src/MemoryBuffer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #ifndef MEMORYBUFFER_HPP_ 25 | #define MEMORYBUFFER_HPP_ 26 | 27 | #include 28 | #include 29 | 30 | class MemoryBuffer 31 | { 32 | public: 33 | explicit MemoryBuffer(size_t size); 34 | 35 | uint8_t *get(); 36 | size_t size() const; 37 | bool isAllocated(); 38 | static MemoryBuffer empty(); 39 | 40 | private: 41 | size_t size_; 42 | std::unique_ptr buffer_; 43 | }; 44 | 45 | #endif // MEMORYBUFFER_H 46 | -------------------------------------------------------------------------------- /src/MemoryBuffer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #include "MemoryBuffer.hpp" 25 | #include 26 | 27 | MemoryBuffer::MemoryBuffer(size_t size) : size_(size) 28 | { 29 | if (size_ > 0) 30 | buffer_ = std::unique_ptr(new (std::nothrow) uint8_t[size]); 31 | } 32 | 33 | uint8_t *MemoryBuffer::get() 34 | { 35 | return buffer_.get(); 36 | } 37 | 38 | size_t MemoryBuffer::size() const 39 | { 40 | return size_; 41 | } 42 | 43 | bool MemoryBuffer::isAllocated() 44 | { 45 | return buffer_.get() != nullptr; 46 | } 47 | 48 | MemoryBuffer MemoryBuffer::empty() 49 | { 50 | return MemoryBuffer(0); 51 | } 52 | -------------------------------------------------------------------------------- /src/CachedTile.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #ifndef CACHEDTILE_HPP_ 25 | #define CACHEDTILE_HPP_ 26 | 27 | #include 28 | 29 | struct CachedTile 30 | { 31 | uint32_t x; 32 | uint32_t y; 33 | uint8_t z; 34 | bool valid; 35 | bool busy; 36 | uint16_t *buffer; 37 | 38 | CachedTile() 39 | : x(0), 40 | y(0), 41 | z(0), 42 | valid(false), 43 | busy(false), 44 | buffer(nullptr) 45 | { 46 | } 47 | 48 | ~CachedTile() 49 | { 50 | free(); 51 | } 52 | 53 | bool allocate(int tileSize) 54 | { 55 | buffer = static_cast(heap_caps_malloc(tileSize * tileSize * sizeof(uint16_t), MALLOC_CAP_SPIRAM)); 56 | return buffer != nullptr; 57 | } 58 | 59 | void free() 60 | { 61 | if (buffer) 62 | { 63 | heap_caps_free(buffer); 64 | buffer = nullptr; 65 | } 66 | valid = false; 67 | busy = false; 68 | } 69 | }; 70 | 71 | static_assert(sizeof(CachedTile) >= 0, "Suppress unusedStruct"); 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /src/ReusableTileFetcher.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #pragma once 25 | 26 | #include 27 | #include 28 | #include 29 | #include "MemoryBuffer.hpp" 30 | 31 | constexpr int OSM_MAX_HEADERLENGTH = 64; 32 | constexpr int OSM_MAX_HOST_LEN = 128; 33 | constexpr int OSM_MAX_PATH_LEN = 128; 34 | constexpr int OSM_DEFAULT_TIMEOUT_MS = 5000; 35 | 36 | class ReusableTileFetcher 37 | { 38 | public: 39 | ReusableTileFetcher(); 40 | ~ReusableTileFetcher(); 41 | 42 | ReusableTileFetcher(const ReusableTileFetcher &) = delete; 43 | ReusableTileFetcher &operator=(const ReusableTileFetcher &) = delete; 44 | 45 | MemoryBuffer fetchToBuffer(const char *url, String &result, unsigned long timeoutMS); 46 | void disconnect(); 47 | 48 | private: 49 | WiFiClient client; 50 | WiFiClientSecure secureClient; 51 | bool currentIsTLS = false; 52 | char currentHost[OSM_MAX_HOST_LEN] = {0}; 53 | char headerLine[OSM_MAX_HEADERLENGTH] = {0}; 54 | uint16_t currentPort = 0; 55 | void setSocket(WiFiClient &c); 56 | 57 | bool parseUrl(const char *url, char *host, char *path, uint16_t &port, bool &useTLS); 58 | bool ensureConnection(const char *host, uint16_t port, bool useTLS, unsigned long timeoutMS, String &result); 59 | void sendHttpRequest(const char *host, const char *path); 60 | bool readHttpHeaders(size_t &contentLength, unsigned long timeoutMS, String &result, bool &connectionClose); 61 | bool readLineWithTimeout(uint32_t timeoutMs); 62 | bool readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeoutMS, String &result); 63 | }; 64 | -------------------------------------------------------------------------------- /src/TileProvider.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #ifndef TILEPROVIDER_HPP_ 25 | #define TILEPROVIDER_HPP_ 26 | 27 | struct TileProvider 28 | { 29 | const char *name; 30 | const char *urlTemplate; 31 | const char *attribution; 32 | bool requiresApiKey; 33 | const char *apiKey; 34 | int maxZoom; 35 | int minZoom; 36 | int tileSize; 37 | }; 38 | 39 | const TileProvider osmStandard = { 40 | "OSM Standard", 41 | "https://tile.openstreetmap.org/%d/%d/%d.png", 42 | "© OpenStreetMap contributors", 43 | false, 44 | "", 45 | 19, 0, 256}; 46 | 47 | const TileProvider ThunderTransportDark256 = { 48 | "Thunderforest Transport Dark 256px", 49 | "https://tile.thunderforest.com/transport-dark/%d/%d/%d.png?apikey=%s", 50 | "© Thunderforest, OpenStreetMap contributors", 51 | true, 52 | "YOUR_THUNDERFOREST_KEY", 53 | 22, 0, 256}; 54 | 55 | const TileProvider ThunderForestCycle512 = { 56 | "Thunderforest Cycle 512px", 57 | "https://tile.thunderforest.com/transport-dark/%d/%d/%d@2x.png?apikey=%s", 58 | "© Thunderforest, OpenStreetMap contributors", 59 | true, 60 | "YOUR_THUNDERFOREST_KEY", 61 | 22, 0, 512}; 62 | 63 | const TileProvider ThunderForestCycle256 = { 64 | "Thunderforest Cycle 256px", 65 | "https://tile.thunderforest.com/cycle/%d/%d/%d.png?apikey=%s", 66 | "© Thunderforest, OpenStreetMap contributors", 67 | true, 68 | "YOUR_THUNDERFOREST_KEY", 69 | 22, 0, 256}; 70 | 71 | // Replace 'YOUR_THUNDERFOREST_KEY' above with a -free- Thunderforest API key 72 | // and uncomment one of the following line to use Thunderforest tiles 73 | 74 | // const TileProvider tileProviders[] = {osmStandard, ThunderTransportDark256, ThunderForestCycle512, ThunderForestCycle256}; 75 | // const TileProvider tileProviders[] = {ThunderTransportDark256}; 76 | // const TileProvider tileProviders[] = {ThunderForestCycle512}; 77 | // const TileProvider tileProviders[] = {ThunderForestCycle256}; 78 | 79 | // If one of the above definitions is used, the following line should be commented out 80 | const TileProvider tileProviders[] = {osmStandard}; 81 | 82 | constexpr int OSM_TILEPROVIDERS = sizeof(tileProviders) / sizeof(TileProvider); 83 | 84 | static_assert(OSM_TILEPROVIDERS > 0, "No TileProvider configured"); 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /src/OpenStreetMap-esp32.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #ifndef OPENSTREETMAP_ESP32_HPP_ 25 | #define OPENSTREETMAP_ESP32_HPP_ 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include "TileProvider.hpp" 36 | #include "CachedTile.hpp" 37 | #include "TileJob.hpp" 38 | #include "MemoryBuffer.hpp" 39 | #include "ReusableTileFetcher.hpp" 40 | #include "fonts/DejaVu9-modded.h" 41 | 42 | constexpr uint16_t OSM_BGCOLOR = lgfx::color565(32, 32, 128); 43 | constexpr UBaseType_t OSM_TASK_PRIORITY = 1; 44 | constexpr uint32_t OSM_TASK_STACKSIZE = 6144; 45 | constexpr uint32_t OSM_JOB_QUEUE_SIZE = 50; 46 | constexpr bool OSM_FORCE_SINGLECORE = false; 47 | constexpr int OSM_SINGLECORE_NUMBER = 1; 48 | 49 | static_assert(OSM_SINGLECORE_NUMBER < 2, "OSM_SINGLECORE_NUMBER must be 0 or 1 (ESP32 has only 2 cores)"); 50 | 51 | using tileList = std::vector>; 52 | using TileBufferList = std::vector; 53 | 54 | namespace 55 | { 56 | PNG *pngCore0 = nullptr; 57 | PNG *pngCore1 = nullptr; 58 | 59 | PNG *getPNGForCore(int coreID) 60 | { 61 | PNG *&ptr = (coreID == 0) ? pngCore0 : pngCore1; 62 | if (!ptr) 63 | { 64 | void *mem = heap_caps_malloc(sizeof(PNG), MALLOC_CAP_SPIRAM); 65 | if (!mem) 66 | return nullptr; 67 | ptr = new (mem) PNG(); 68 | } 69 | return ptr; 70 | } 71 | 72 | PNG *getPNGCurrentCore() 73 | { 74 | return getPNGForCore(xPortGetCoreID()); 75 | } 76 | } 77 | 78 | class OpenStreetMap 79 | { 80 | public: 81 | OpenStreetMap() = default; 82 | OpenStreetMap(const OpenStreetMap &) = delete; 83 | OpenStreetMap &operator=(const OpenStreetMap &) = delete; 84 | OpenStreetMap(OpenStreetMap &&other) = delete; 85 | OpenStreetMap &operator=(OpenStreetMap &&other) = delete; 86 | 87 | ~OpenStreetMap(); 88 | 89 | void setSize(uint16_t w, uint16_t h); 90 | uint16_t tilesNeeded(uint16_t mapWidth, uint16_t mapHeight); 91 | bool resizeTilesCache(uint16_t numberOfTiles); 92 | bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS = 0); 93 | inline void freeTilesCache(); 94 | 95 | bool setTileProvider(int index); 96 | const char *getProviderName() { return currentProvider->name; }; 97 | int getMinZoom() const { return currentProvider->minZoom; }; 98 | int getMaxZoom() const { return currentProvider->maxZoom; }; 99 | 100 | private: 101 | double lon2tile(double lon, uint8_t zoom); 102 | double lat2tile(double lat, uint8_t zoom); 103 | void computeRequiredTiles(double longitude, double latitude, uint8_t zoom, tileList &requiredTiles); 104 | void updateCache(const tileList &requiredTiles, uint8_t zoom, TileBufferList &tilePointers); 105 | bool startTileWorkerTasks(); 106 | void makeJobList(const tileList &requiredTiles, std::vector &jobs, uint8_t zoom, TileBufferList &tilePointers); 107 | void runJobs(const std::vector &jobs); 108 | CachedTile *findUnusedTile(const tileList &requiredTiles, uint8_t zoom); 109 | CachedTile *isTileCached(uint32_t x, uint32_t y, uint8_t z); 110 | bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeoutMS); 111 | bool composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePointers); 112 | static void tileFetcherTask(void *param); 113 | static void PNGDraw(PNGDRAW *pDraw); 114 | void invalidateTile(CachedTile *tile); 115 | 116 | static inline thread_local OpenStreetMap *currentInstance = nullptr; 117 | static inline thread_local uint16_t *currentTileBuffer = nullptr; 118 | const TileProvider *currentProvider = &tileProviders[0]; 119 | std::vector tilesCache; 120 | 121 | TaskHandle_t ownerTask = nullptr; 122 | int numberOfWorkers = 0; 123 | QueueHandle_t jobQueue = nullptr; 124 | std::atomic pendingJobs = 0; 125 | bool tasksStarted = false; 126 | 127 | unsigned long mapTimeoutMS = 0; // 0 means no timeout 128 | unsigned long startJobsMS = 0; 129 | 130 | uint16_t mapWidth = 320; 131 | uint16_t mapHeight = 240; 132 | 133 | int16_t startOffsetX = 0; 134 | int16_t startOffsetY = 0; 135 | 136 | int32_t startTileIndexX = 0; 137 | int32_t startTileIndexY = 0; 138 | 139 | uint16_t numberOfColums = 0; 140 | }; 141 | 142 | #endif 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OpenStreetMap-esp32 2 | 3 | [![License](https://img.shields.io/github/license/CelliesProjects/OpenStreetMap-esp32)](https://github.com/CelliesProjects/OpenStreetMap-esp32/blob/main/LICENSE) 4 | [![Release](https://img.shields.io/github/v/release/CelliesProjects/OpenStreetMap-esp32)](https://github.com/CelliesProjects/OpenStreetMap-esp32/releases/latest) 5 | [![Issues](https://img.shields.io/github/issues/CelliesProjects/OpenStreetMap-esp32)](https://github.com/CelliesProjects/OpenStreetMap-esp32/issues) 6 | [![PlatformIO](https://img.shields.io/badge/PlatformIO-Compatible-green?logo=platformio)](https://registry.platformio.org/libraries/celliesprojects/openstreetmap-esp32) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/0961fc2320cd495a9411eb391d5791ca)](https://app.codacy.com/gh/CelliesProjects/OpenStreetMap-esp32/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 8 | 9 | This PlatformIO library provides a [OpenStreetMap](https://www.openstreetmap.org/) (OSM) map fetching and tile caching system for ESP32-based devices. 10 | Under the hood it uses [LovyanGFX](https://github.com/lovyan03/LovyanGFX) and [PNGdec](https://github.com/bitbank2/PNGdec) to do the heavy lifting. 11 | 12 | [![map](https://github.com/user-attachments/assets/39a7f287-c59d-4365-888a-d4c3f77a1dd1 "Click to visit OpenStreetMap.org")](https://www.openstreetmap.org/) 13 | 14 | A map is composed from downloaded OSM tiles and returned as a LGFX sprite. 15 | Tile fetching and decoding is performed concurrently across both cores on dualcore ESP32 devices. 16 | A composed map can be pushed to the screen, saved to SD or used for further composing. 17 | Downloaded tiles are cached in psram for reuse. 18 | 19 | This should work on any ESP32 type with psram and a LovyanGFX compatible display. 20 | OSM tiles are quite large at 128kB or insane large at 512kB per tile, so psram is required. 21 | 22 | ### Multiple tile formats and providers are supported 23 | 24 | You can switch provider and tile format at runtime, or set up a different default tile provider if you want. 25 | This library can do it all and is very easy to configure and use. 26 | 27 | ### TLS validation note 28 | 29 | This project currently uses `setInsecure()` for `WiFiClientSecure` connections, which disables certificate validation. 30 | 31 | - Risk: Without TLS validation, responses could in theory be intercepted or altered. 32 | This matters most if requests carry secrets (e.g. API keys). 33 | 34 | - Practical impact: Standard OpenStreetMap tile servers do not require API keys or credentials. 35 | In this case, the main risk is limited to someone tampering with map images. 36 | 37 | - Benefit: Simplifies setup and supports multiple tile providers without needing to manage CA certificates. 38 | 39 | ## How to use 40 | 41 | This library is **PlatformIO only** due to use of modern C++ features. The Arduino IDE is **not** supported. 42 | Use [the latest Arduino ESP32 Core version](https://github.com/pioarduino/platform-espressif32/releases/latest) from [pioarduino](https://github.com/pioarduino/platform-espressif32) to compile this library. 43 | 44 | See the example PIO settings and example code to get started. 45 | 46 | ### Example `platformio.ini` settings 47 | 48 | These settings use `Arduino Release v3.2.0 based on ESP-IDF v5.4.1` from pioarduino. 49 | 50 | ```bash 51 | [env] 52 | platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip 53 | framework = arduino 54 | 55 | lib_deps = 56 | celliesprojects/OpenStreetMap-esp32@^1.2.2 57 | ``` 58 | 59 | ## Functions 60 | 61 | ### Get the minimum zoom level 62 | 63 | ```c++ 64 | int getMinZoom() 65 | ``` 66 | 67 | ### Get the maximum zoom level 68 | 69 | ```c++ 70 | int getMaxZoom() 71 | ``` 72 | 73 | ### Set map size 74 | 75 | ```c++ 76 | void setSize(uint16_t w, uint16_t h) 77 | ``` 78 | 79 | - If no size is set a 320px by 240px map will be returned. 80 | - The tile cache might need resizing if the size is increased. 81 | 82 | ### Get the number of tiles needed to cache a map 83 | 84 | ```c++ 85 | uint16_t tilesNeeded(uint16_t w, uint16_t h) 86 | ``` 87 | 88 | This returns the -most pessimistic- number of tiles required to cache the given map size. 89 | 90 | ### Resize the tiles cache 91 | 92 | ```c++ 93 | bool resizeTilesCache(uint16_t numberOfTiles) 94 | ``` 95 | 96 | - The cache content is cleared before resizing. 97 | - Each 256px tile allocates **128kB** psram. 98 | - Each 512px tile allocates **512kB** psram. 99 | 100 | **Don't over-allocate** 101 | When resizing the cache, keep in mind that the map sprite also uses psram. 102 | The PNG decoders -~50kB for each core- also live in psram. 103 | Use the above `tilesNeeded` function to calculate a safe and sane cache size if you change the map size. 104 | 105 | ### Fetch a map 106 | 107 | ```c++ 108 | bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS = 0) 109 | ``` 110 | 111 | - Overflowing `longitude` are wrapped and normalized to +-180°. 112 | - Overflowing `latitude` are clamped to +-85°. 113 | - Valid range for the `zoom` level is from `getMinZoom()` to `getMaxZoom()`. 114 | - `timeoutMS` can be used to throttle the amount of downloaded tiles per call. 115 | Setting it to anything other than `0` sets a timeout. Sane values start around ~100ms. 116 | **Note:** No more tile downloads will be started after the timeout expires, but tiles that are downloading will be finished. 117 | **Note:** You might end up with missing map tiles. Or no map at all if you set the timeout too short. 118 | 119 | ### Free the psram memory used by the tile cache 120 | 121 | ```c++ 122 | void freeTilesCache() 123 | ``` 124 | 125 | - Does **not** free the PNG decoder(s). 126 | 127 | ### Switch to a different tile provider 128 | 129 | ```c++ 130 | bool setTileProvider(int index) 131 | ``` 132 | 133 | This function will switch to tile provider `index` defined in `src/TileProvider.hpp`. 134 | 135 | - Returns `true` and clears the cache on success. 136 | - Returns `false` -and the current tile provider is unchanged- if no provider at the index is defined. 137 | 138 | ### Get the number of defined providers 139 | 140 | `OSM_TILEPROVIDERS` gives the number of defined providers. 141 | 142 | Example use: 143 | 144 | ```c++ 145 | const int numberOfProviders = OSM_TILEPROVIDERS; 146 | ``` 147 | 148 | **Note:** In the default setup there is only one provider defined. 149 | 150 | ### Get the provider name 151 | 152 | ```c++ 153 | char *getProviderName() 154 | ``` 155 | 156 | ## Adding tile providers 157 | 158 | See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require you to register for a **free** API key and adjusting/uncommenting 2 lines in the config. 159 | Register for a ThunderForest free tier [here](https://manage.thunderforest.com/users/sign_up?price=hobby-project-usd) without needing a creditcard to sign up. 160 | 161 | If you encounter a problem or want to request support for a new provider, please check the [issue tracker](../../issues) for existing reports or [open an issue](../../issues/new). 162 | 163 | ## Example code 164 | 165 | ### Example returning the default 320x240 map 166 | 167 | ```c++ 168 | #include 169 | #include 170 | 171 | #define LGFX_M5STACK_CORE2 // for supported devices see 172 | // https://github.com/lovyan03/LovyanGFX 173 | 174 | #include 175 | #include 176 | 177 | #include 178 | 179 | const char *ssid = "xxx"; 180 | const char *password = "xxx"; 181 | 182 | LGFX display; 183 | OpenStreetMap osm; 184 | 185 | double longitude = 5.9; 186 | double latitude = 51.5; 187 | int zoom = 5; 188 | 189 | void setup() 190 | { 191 | Serial.begin(115200); 192 | Serial.printf("WiFi connecting to %s\n", ssid); 193 | 194 | WiFi.begin(ssid, password); 195 | while (WiFi.status() != WL_CONNECTED) 196 | { 197 | delay(10); 198 | Serial.print("."); 199 | } 200 | 201 | Serial.println("\nWiFi connected"); 202 | 203 | display.begin(); 204 | display.setRotation(1); 205 | display.setBrightness(110); 206 | 207 | // create a sprite to store the map 208 | LGFX_Sprite map(&display); 209 | 210 | // returned map is 320px by 240px by default 211 | const bool success = osm.fetchMap(map, longitude, latitude, zoom); 212 | 213 | if (success) 214 | map.pushSprite(0, 0); 215 | else 216 | Serial.println("Failed to fetch map."); 217 | } 218 | 219 | void loop() 220 | { 221 | delay(1000); 222 | } 223 | ``` 224 | 225 | ### Example setting map resolution and cache size on RGB panel devices 226 | 227 | ```c++ 228 | #include 229 | #include 230 | #include 231 | 232 | #include "LGFX_ESP32_8048S050C.hpp" // replace with your panel config 233 | 234 | #include 235 | 236 | const char *ssid = "xxx"; 237 | const char *password = "xxx"; 238 | 239 | LGFX display; 240 | OpenStreetMap osm; 241 | 242 | int mapWidth = 480; 243 | int mapHeight = 800; 244 | int cacheSize = 20; // cache size in tiles where each osm tile is 128kB 245 | double longitude = 5.9; 246 | double latitude = 51.5; 247 | int zoom = 5; 248 | 249 | void setup() 250 | { 251 | Serial.begin(115200); 252 | Serial.printf("WiFi connecting to %s\n", ssid); 253 | 254 | WiFi.begin(ssid, password); 255 | while (WiFi.status() != WL_CONNECTED) 256 | { 257 | delay(10); 258 | Serial.print("."); 259 | } 260 | 261 | Serial.println("\nWiFi connected"); 262 | 263 | display.begin(); 264 | display.setRotation(1); 265 | display.setBrightness(110); 266 | 267 | osm.resizeTilesCache(cacheSize); 268 | osm.setSize(mapWidth, mapHeight); 269 | 270 | LGFX_Sprite map(&display); 271 | 272 | const bool success = osm.fetchMap(map, longitude, latitude, zoom); 273 | if (success) 274 | { 275 | // Draw a crosshair on the map 276 | map.drawLine(0, map.height() / 2, map.width(), map.height() / 2, 0); 277 | map.drawLine(map.width() / 2, 0, map.width() / 2, map.height(), 0); 278 | 279 | map.pushSprite(0, 0); 280 | } 281 | else 282 | Serial.println("Failed to fetch map."); 283 | } 284 | 285 | void loop() 286 | { 287 | delay(1000); 288 | } 289 | ``` 290 | 291 | ## License differences between this library and the map data 292 | 293 | ### This library has a MIT license 294 | 295 | The `OpenstreetMap-esp32` library -this library- is licensed under the [MIT license](/LICENSE). 296 | 297 | ### The downloaded tile data has a ODbL license 298 | 299 | OpenStreetMap® is open data, licensed under the [Open Data Commons Open Database License (ODbL)](https://opendatacommons.org/licenses/odbl/) by the OpenStreetMap Foundation (OSMF). 300 | 301 | Use of any OSMF provided service is governed by the [OSMF Terms of Use](https://osmfoundation.org/wiki/Terms_of_Use). -------------------------------------------------------------------------------- /src/fonts/DejaVu9-modded.h: -------------------------------------------------------------------------------- 1 | /* DejaVu 9 2 | original ttf url : https://dejavu-fonts.github.io/ 3 | original license : https://dejavu-fonts.github.io/License.html 4 | This data has been converted to AdafruitGFX font format from DejaVuSans.ttf. 5 | 6 | Modded by Cellie - added a © sign 0xA9 with https://tchapi.github.io/Adafruit-GFX-Font-Customiser/ 7 | */ 8 | const uint8_t DejaVu9Bitmaps[] PROGMEM = { 9 | 0xFA, 0xFA, 0xB4, 0x28, 0xAF, 0xCA, 0xFD, 0x45, 0x00, 0x21, 0xEA, 0x38, 10 | 0x38, 0xAF, 0x08, 0x44, 0xA4, 0xA8, 0x5A, 0x15, 0x25, 0x22, 0x31, 0x04, 11 | 0x19, 0x9E, 0x66, 0xC0, 0xC0, 0x4A, 0xA1, 0x85, 0x52, 0xAB, 0x9D, 0x50, 12 | 0x21, 0x3E, 0x42, 0x00, 0xC0, 0xC0, 0x80, 0x25, 0x25, 0x20, 0x69, 0x99, 13 | 0x99, 0x60, 0xC9, 0x24, 0xB8, 0x64, 0x84, 0x44, 0x43, 0xC0, 0x69, 0x16, 14 | 0x11, 0x60, 0x11, 0x94, 0xA9, 0x7C, 0x40, 0xF8, 0x8E, 0x11, 0xE0, 0x7C, 15 | 0x8E, 0x99, 0x60, 0xF1, 0x22, 0x24, 0x40, 0x69, 0x96, 0x99, 0x60, 0x69, 16 | 0x97, 0x13, 0xE0, 0x88, 0x8C, 0x04, 0xEE, 0x0E, 0x04, 0xFC, 0x0F, 0xC0, 17 | 0x81, 0xC1, 0xDC, 0x80, 0xE1, 0x24, 0x40, 0x40, 0x3C, 0x42, 0x9D, 0xA5, 18 | 0xA5, 0x9E, 0x40, 0x38, 0x30, 0xC4, 0x92, 0x7A, 0x18, 0x40, 0xF4, 0x63, 19 | 0xE8, 0xC7, 0xC0, 0x72, 0x61, 0x08, 0x25, 0xC0, 0xF4, 0xE3, 0x18, 0xCF, 20 | 0xC0, 0xF8, 0x8F, 0x88, 0xF0, 0xF8, 0x8F, 0x88, 0x80, 0x76, 0x61, 0x38, 21 | 0xE5, 0xC0, 0x8C, 0x63, 0xF8, 0xC6, 0x20, 0xFE, 0x55, 0x55, 0x80, 0x8C, 22 | 0xA9, 0x8A, 0x4A, 0x20, 0x88, 0x88, 0x88, 0xF0, 0x87, 0x3C, 0xED, 0xB6, 23 | 0x18, 0x40, 0x8E, 0x73, 0x59, 0xCE, 0x20, 0x76, 0xE3, 0x18, 0xED, 0xC0, 24 | 0xE9, 0x9E, 0x88, 0x80, 0x76, 0xE3, 0x18, 0xE9, 0xC2, 0xE4, 0xA5, 0xCA, 25 | 0x4A, 0x20, 0x72, 0x28, 0x1C, 0x0A, 0x27, 0x00, 0xF9, 0x08, 0x42, 0x10, 26 | 0x80, 0x8C, 0x63, 0x18, 0xC5, 0xC0, 0x86, 0x14, 0x92, 0x48, 0xC3, 0x00, 27 | 0x49, 0x24, 0x8A, 0x85, 0x43, 0xE0, 0xA0, 0x50, 0xCD, 0x23, 0x0C, 0x31, 28 | 0x28, 0xC0, 0x8A, 0x9C, 0x42, 0x10, 0x80, 0xF8, 0x44, 0x44, 0x43, 0xE0, 29 | 0xEA, 0xAB, 0x91, 0x24, 0x48, 0xD5, 0x57, 0x31, 0x20, 0xF8, 0x90, 0x61, 30 | 0x79, 0xF0, 0x88, 0x8E, 0x99, 0x9E, 0x78, 0x88, 0x70, 0x11, 0x17, 0x99, 31 | 0x97, 0x69, 0xF8, 0x70, 0x34, 0x4E, 0x44, 0x44, 0x79, 0x99, 0x71, 0x60, 32 | 0x88, 0x8E, 0x99, 0x99, 0xBE, 0x45, 0x55, 0x80, 0x88, 0x89, 0xAC, 0xA9, 33 | 0xFF, 0xED, 0x26, 0x4C, 0x99, 0x20, 0xE9, 0x99, 0x90, 0x69, 0x99, 0x60, 34 | 0xE9, 0x99, 0xE8, 0x80, 0x79, 0x99, 0x71, 0x10, 0xF2, 0x48, 0x68, 0x62, 35 | 0xE0, 0x4F, 0x44, 0x47, 0x99, 0x99, 0x70, 0x44, 0x98, 0xA1, 0xC1, 0x00, 36 | 0x93, 0x76, 0xBA, 0x24, 0x40, 0x8A, 0x88, 0xA8, 0x80, 0x44, 0x88, 0xA1, 37 | 0xC1, 0x02, 0x18, 0x00, 0xF1, 0x24, 0xF0, 0x69, 0x64, 0x93, 0xFF, 0x80, 38 | 0xC9, 0x34, 0x96, 0x01, 0x91, 0x80, 0x3C, 0x42, 0x99, 0xA5, 0xA1, 0xA5, 39 | 0x99, 0x42, 0x3C 40 | }; 41 | 42 | const GFXglyph DejaVu9Glyphs[] PROGMEM = { 43 | { 0, 0, 0, 4, 0, 1 }, // 0x20 ' ' 44 | { 1, 1, 7, 4, 1, -6 }, // 0x21 '!' 45 | { 2, 3, 2, 5, 1, -6 }, // 0x22 '"' 46 | { 3, 6, 7, 9, 1, -6 }, // 0x23 '#' 47 | { 9, 6, 8, 7, 0, -6 }, // 0x24 '$' 48 | { 15, 8, 7, 10, 0, -6 }, // 0x25 '%' 49 | { 22, 6, 7, 9, 1, -6 }, // 0x26 '&' 50 | { 28, 1, 2, 3, 1, -6 }, // 0x27 ''' 51 | { 29, 2, 8, 5, 1, -7 }, // 0x28 '(' 52 | { 31, 2, 8, 5, 1, -7 }, // 0x29 ')' 53 | { 33, 5, 4, 6, 0, -6 }, // 0x2A '*' 54 | { 36, 5, 5, 9, 1, -4 }, // 0x2B '+' 55 | { 40, 1, 2, 4, 1, 0 }, // 0x2C ',' 56 | { 41, 2, 1, 4, 1, -2 }, // 0x2D '-' 57 | { 42, 1, 1, 4, 1, 0 }, // 0x2E '.' 58 | { 43, 3, 7, 4, 0, -6 }, // 0x2F '/' 59 | { 46, 4, 7, 7, 1, -6 }, // 0x30 '0' 60 | { 50, 3, 7, 7, 2, -6 }, // 0x31 '1' 61 | { 53, 5, 7, 7, 1, -6 }, // 0x32 '2' 62 | { 58, 4, 7, 7, 1, -6 }, // 0x33 '3' 63 | { 62, 5, 7, 7, 1, -6 }, // 0x34 '4' 64 | { 67, 4, 7, 7, 1, -6 }, // 0x35 '5' 65 | { 71, 4, 7, 7, 1, -6 }, // 0x36 '6' 66 | { 75, 4, 7, 7, 1, -6 }, // 0x37 '7' 67 | { 79, 4, 7, 7, 1, -6 }, // 0x38 '8' 68 | { 83, 4, 7, 7, 1, -6 }, // 0x39 '9' 69 | { 87, 1, 5, 4, 1, -4 }, // 0x3A ':' 70 | { 88, 1, 6, 4, 1, -4 }, // 0x3B ';' 71 | { 89, 6, 5, 9, 1, -4 }, // 0x3C '<' 72 | { 93, 6, 3, 9, 1, -3 }, // 0x3D '=' 73 | { 96, 6, 5, 9, 1, -4 }, // 0x3E '>' 74 | { 100, 4, 7, 6, 1, -6 }, // 0x3F '?' 75 | { 104, 8, 8, 11, 1, -6 }, // 0x40 '@' 76 | { 112, 6, 7, 7, 0, -6 }, // 0x41 'A' 77 | { 118, 5, 7, 8, 1, -6 }, // 0x42 'B' 78 | { 123, 5, 7, 8, 1, -6 }, // 0x43 'C' 79 | { 128, 5, 7, 8, 1, -6 }, // 0x44 'D' 80 | { 133, 4, 7, 7, 1, -6 }, // 0x45 'E' 81 | { 137, 4, 7, 7, 1, -6 }, // 0x46 'F' 82 | { 141, 5, 7, 8, 1, -6 }, // 0x47 'G' 83 | { 146, 5, 7, 8, 1, -6 }, // 0x48 'H' 84 | { 151, 1, 7, 4, 1, -6 }, // 0x49 'I' 85 | { 152, 2, 9, 4, 0, -6 }, // 0x4A 'J' 86 | { 155, 5, 7, 7, 1, -6 }, // 0x4B 'K' 87 | { 160, 4, 7, 6, 1, -6 }, // 0x4C 'L' 88 | { 164, 6, 7, 9, 1, -6 }, // 0x4D 'M' 89 | { 170, 5, 7, 8, 1, -6 }, // 0x4E 'N' 90 | { 175, 5, 7, 8, 1, -6 }, // 0x4F 'O' 91 | { 180, 4, 7, 7, 1, -6 }, // 0x50 'P' 92 | { 184, 5, 8, 8, 1, -6 }, // 0x51 'Q' 93 | { 189, 5, 7, 7, 1, -6 }, // 0x52 'R' 94 | { 194, 6, 7, 8, 1, -6 }, // 0x53 'S' 95 | { 200, 5, 7, 6, 0, -6 }, // 0x54 'T' 96 | { 205, 5, 7, 8, 1, -6 }, // 0x55 'U' 97 | { 210, 6, 7, 7, 0, -6 }, // 0x56 'V' 98 | { 216, 9, 7, 8, -1, -6 }, // 0x57 'W' 99 | { 224, 6, 7, 7, 0, -6 }, // 0x58 'X' 100 | { 230, 5, 7, 6, 0, -6 }, // 0x59 'Y' 101 | { 235, 5, 7, 6, 0, -6 }, // 0x5A 'Z' 102 | { 240, 2, 8, 5, 1, -6 }, // 0x5B '[' 103 | { 242, 3, 7, 4, 0, -6 }, // 0x5C '\' 104 | { 245, 2, 8, 5, 1, -6 }, // 0x5D ']' 105 | { 247, 6, 2, 9, 1, -6 }, // 0x5E '^' 106 | { 249, 5, 1, 6, 0, 2 }, // 0x5F '_' 107 | { 250, 2, 2, 6, 1, -7 }, // 0x60 '`' 108 | { 251, 4, 5, 7, 1, -4 }, // 0x61 'a' 109 | { 254, 4, 8, 7, 1, -7 }, // 0x62 'b' 110 | { 258, 4, 5, 7, 1, -4 }, // 0x63 'c' 111 | { 261, 4, 8, 7, 1, -7 }, // 0x64 'd' 112 | { 265, 4, 5, 7, 1, -4 }, // 0x65 'e' 113 | { 268, 4, 8, 4, 0, -7 }, // 0x66 'f' 114 | { 272, 4, 7, 7, 1, -4 }, // 0x67 'g' 115 | { 276, 4, 8, 7, 1, -7 }, // 0x68 'h' 116 | { 280, 1, 7, 4, 1, -6 }, // 0x69 'i' 117 | { 281, 2, 9, 4, 0, -6 }, // 0x6A 'j' 118 | { 284, 4, 8, 6, 1, -7 }, // 0x6B 'k' 119 | { 288, 1, 8, 4, 1, -7 }, // 0x6C 'l' 120 | { 289, 7, 5, 10, 1, -4 }, // 0x6D 'm' 121 | { 294, 4, 5, 7, 1, -4 }, // 0x6E 'n' 122 | { 297, 4, 5, 7, 1, -4 }, // 0x6F 'o' 123 | { 300, 4, 7, 7, 1, -4 }, // 0x70 'p' 124 | { 304, 4, 7, 7, 1, -4 }, // 0x71 'q' 125 | { 308, 3, 5, 5, 1, -4 }, // 0x72 'r' 126 | { 310, 4, 5, 6, 1, -4 }, // 0x73 's' 127 | { 313, 4, 6, 5, 0, -5 }, // 0x74 't' 128 | { 316, 4, 5, 7, 1, -4 }, // 0x75 'u' 129 | { 319, 7, 5, 6, -1, -4 }, // 0x76 'v' 130 | { 324, 7, 5, 8, 0, -4 }, // 0x77 'w' 131 | { 329, 5, 5, 6, 0, -4 }, // 0x78 'x' 132 | { 333, 7, 7, 6, -1, -4 }, // 0x79 'y' 133 | { 340, 4, 5, 7, 1, -4 }, // 0x7A 'z' 134 | { 343, 3, 8, 6, 1, -6 }, // 0x7B '{' 135 | { 346, 1, 9, 4, 1, -6 }, // 0x7C '|' 136 | { 348, 3, 8, 6, 1, -6 }, // 0x7D '}' 137 | { 351, 6, 3, 9, 1, -4 }, // 0x7E '~' 138 | { 0, 0, 0, 0, 0, 0 }, // 0x7F 'non-printable' 139 | { 0, 0, 0, 0, 0, 0 }, // 0x80 'non-printable' 140 | { 0, 0, 0, 0, 0, 0 }, // 0x81 'non-printable' 141 | { 0, 0, 0, 0, 0, 0 }, // 0x82 'non-printable' 142 | { 0, 0, 0, 0, 0, 0 }, // 0x83 'non-printable' 143 | { 0, 0, 0, 0, 0, 0 }, // 0x84 'non-printable' 144 | { 0, 0, 0, 0, 0, 0 }, // 0x85 'non-printable' 145 | { 0, 0, 0, 0, 0, 0 }, // 0x86 'non-printable' 146 | { 0, 0, 0, 0, 0, 0 }, // 0x87 'non-printable' 147 | { 0, 0, 0, 0, 0, 0 }, // 0x88 'non-printable' 148 | { 0, 0, 0, 0, 0, 0 }, // 0x89 'non-printable' 149 | { 0, 0, 0, 0, 0, 0 }, // 0x8A 'non-printable' 150 | { 0, 0, 0, 0, 0, 0 }, // 0x8B 'non-printable' 151 | { 0, 0, 0, 0, 0, 0 }, // 0x8C 'non-printable' 152 | { 0, 0, 0, 0, 0, 0 }, // 0x8D 'non-printable' 153 | { 0, 0, 0, 0, 0, 0 }, // 0x8E 'non-printable' 154 | { 0, 0, 0, 0, 0, 0 }, // 0x8F 'non-printable' 155 | { 0, 0, 0, 0, 0, 0 }, // 0x90 'non-printable' 156 | { 0, 0, 0, 0, 0, 0 }, // 0x91 'non-printable' 157 | { 0, 0, 0, 0, 0, 0 }, // 0x92 'non-printable' 158 | { 0, 0, 0, 0, 0, 0 }, // 0x93 'non-printable' 159 | { 0, 0, 0, 0, 0, 0 }, // 0x94 'non-printable' 160 | { 0, 0, 0, 0, 0, 0 }, // 0x95 'non-printable' 161 | { 0, 0, 0, 0, 0, 0 }, // 0x96 'non-printable' 162 | { 0, 0, 0, 0, 0, 0 }, // 0x97 'non-printable' 163 | { 0, 0, 0, 0, 0, 0 }, // 0x98 'non-printable' 164 | { 0, 0, 0, 0, 0, 0 }, // 0x99 'non-printable' 165 | { 0, 0, 0, 0, 0, 0 }, // 0x9A 'non-printable' 166 | { 0, 0, 0, 0, 0, 0 }, // 0x9B 'non-printable' 167 | { 0, 0, 0, 0, 0, 0 }, // 0x9C 'non-printable' 168 | { 0, 0, 0, 0, 0, 0 }, // 0x9D 'non-printable' 169 | { 0, 0, 0, 0, 0, 0 }, // 0x9E 'non-printable' 170 | { 0, 0, 0, 0, 0, 0 }, // 0x9F 'non-printable' 171 | { 0, 0, 0, 0, 0, 0 }, // 0xA0 ' ' 172 | { 0, 0, 0, 0, 0, 0 }, // 0xA1 '¡' 173 | { 0, 0, 0, 0, 0, 0 }, // 0xA2 '¢' 174 | { 0, 0, 0, 0, 0, 0 }, // 0xA3 '£' 175 | { 0, 0, 0, 0, 0, 0 }, // 0xA4 '¤' 176 | { 0, 0, 0, 0, 0, 0 }, // 0xA5 '¥' 177 | { 0, 0, 0, 0, 0, 0 }, // 0xA6 '¦' 178 | { 0, 0, 0, 0, 0, 0 }, // 0xA7 '§' 179 | { 0, 0, 0, 0, 0, 0 }, // 0xA8 '¨' 180 | { 354, 8, 9, 10, 1, -7 } // 0xA9 '©' 181 | }; 182 | 183 | const GFXfont DejaVu9Modded PROGMEM = { 184 | (uint8_t*)DejaVu9Bitmaps, 185 | (GFXglyph*)DejaVu9Glyphs, 0x20, 0xA9, 10 }; 186 | 187 | -------------------------------------------------------------------------------- /src/ReusableTileFetcher.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #include "ReusableTileFetcher.hpp" 25 | 26 | ReusableTileFetcher::ReusableTileFetcher() {} 27 | ReusableTileFetcher::~ReusableTileFetcher() { disconnect(); } 28 | 29 | void ReusableTileFetcher::sendHttpRequest(const char *host, const char *path) 30 | { 31 | Stream *s = currentIsTLS ? static_cast(&secureClient) : static_cast(&client); 32 | 33 | char buf[256]; 34 | snprintf(buf, sizeof(buf), "GET %s HTTP/1.1\r\nHost: %s\r\n", path, host); 35 | s->print(buf); 36 | s->print("User-Agent: OpenStreetMap-esp32/1.0 (+https://github.com/CelliesProjects/OpenStreetMap-esp32)\r\nConnection: keep-alive\r\n\r\n"); 37 | } 38 | 39 | void ReusableTileFetcher::disconnect() 40 | { 41 | if (currentIsTLS) 42 | secureClient.stop(); 43 | else 44 | client.stop(); 45 | currentHost[0] = 0; 46 | currentPort = 0; 47 | currentIsTLS = false; 48 | } 49 | 50 | MemoryBuffer ReusableTileFetcher::fetchToBuffer(const char *url, String &result, unsigned long timeoutMS) 51 | { 52 | char host[OSM_MAX_HOST_LEN]; 53 | char path[OSM_MAX_PATH_LEN]; 54 | uint16_t port; 55 | bool useTLS; 56 | 57 | log_d("url: %s", url); 58 | 59 | [[maybe_unused]] const unsigned long startMS = millis(); 60 | 61 | if (!parseUrl(url, host, path, port, useTLS)) 62 | { 63 | result = "Invalid URL"; 64 | return MemoryBuffer::empty(); 65 | } 66 | 67 | if (!ensureConnection(host, port, useTLS, timeoutMS, result)) 68 | return MemoryBuffer::empty(); 69 | 70 | sendHttpRequest(host, path); 71 | 72 | size_t contentLength = 0; 73 | bool connClose = false; 74 | 75 | if (!readHttpHeaders(contentLength, timeoutMS, result, connClose)) 76 | { 77 | disconnect(); 78 | return MemoryBuffer::empty(); 79 | } 80 | 81 | if (contentLength == 0) 82 | { 83 | result = "Empty response (Content-Length=0)"; 84 | disconnect(); 85 | return MemoryBuffer::empty(); 86 | } 87 | 88 | auto buffer = MemoryBuffer(contentLength); 89 | if (!buffer.isAllocated()) 90 | { 91 | result = "Download buffer allocation failed"; 92 | disconnect(); 93 | return MemoryBuffer::empty(); 94 | } 95 | 96 | if (!readBody(buffer, contentLength, timeoutMS, result)) 97 | { 98 | disconnect(); 99 | return MemoryBuffer::empty(); 100 | } 101 | 102 | log_d("fetching %s took %lu ms", url, millis() - startMS); 103 | 104 | // Server requested connection close → drop it 105 | if (connClose) 106 | disconnect(); 107 | 108 | return buffer; 109 | } 110 | 111 | bool ReusableTileFetcher::parseUrl(const char *url, char *host, char *path, uint16_t &port, bool &useTLS) 112 | { 113 | if (!url) 114 | return false; 115 | 116 | if (strncmp(url, "https://", 8) == 0) 117 | { 118 | useTLS = true; 119 | port = 443; 120 | } 121 | else if (strncmp(url, "http://", 7) == 0) 122 | { 123 | useTLS = false; 124 | port = 80; 125 | } 126 | else 127 | return false; 128 | 129 | int idxHostStart = useTLS ? 8 : 7; // skip scheme 130 | const char *pathPtr = strchr(url + idxHostStart, '/'); 131 | if (!pathPtr) 132 | return false; // no '/' → invalid 133 | 134 | int hostLen = pathPtr - (url + idxHostStart); 135 | if (hostLen <= 0 || hostLen >= OSM_MAX_HOST_LEN) 136 | return false; // too long for buffer 137 | 138 | snprintf(host, OSM_MAX_HOST_LEN, "%.*s", hostLen, url + idxHostStart); 139 | 140 | int pathLen = strnlen(pathPtr, OSM_MAX_PATH_LEN); 141 | if (pathLen == 0 || pathLen >= OSM_MAX_PATH_LEN) 142 | return false; // too long for buffer 143 | 144 | snprintf(path, OSM_MAX_PATH_LEN, "%s", pathPtr); 145 | 146 | return true; 147 | } 148 | 149 | void ReusableTileFetcher::setSocket(WiFiClient &c) 150 | { 151 | c.setNoDelay(true); 152 | c.setTimeout(OSM_DEFAULT_TIMEOUT_MS); 153 | } 154 | 155 | bool ReusableTileFetcher::ensureConnection(const char *host, uint16_t port, bool useTLS, unsigned long timeoutMS, String &result) 156 | { 157 | // If we already have a connection to exact host/port/scheme and it's connected, keep it. 158 | if ((useTLS == currentIsTLS) && !strcmp(host, currentHost) && (port == currentPort) && 159 | ((useTLS && secureClient.connected()) || (!useTLS && client.connected()))) 160 | { 161 | return true; 162 | } 163 | 164 | disconnect(); 165 | 166 | uint32_t connectTimeout = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS; 167 | 168 | if (useTLS) 169 | { 170 | secureClient.setInsecure(); 171 | if (!secureClient.connect(host, port, connectTimeout)) 172 | { 173 | result = "TLS connect failed to "; 174 | result += host; 175 | return false; 176 | } 177 | setSocket(secureClient); 178 | currentIsTLS = true; 179 | } 180 | else 181 | { 182 | if (!client.connect(host, port, connectTimeout)) 183 | { 184 | result = "TCP connect failed to "; 185 | result += host; 186 | return false; 187 | } 188 | setSocket(client); 189 | currentIsTLS = false; 190 | } 191 | snprintf(currentHost, sizeof(currentHost), "%s", host); 192 | currentPort = port; 193 | log_i("(Re)connected on core %i to %s:%u (TLS=%d) (timeout=%lu ms)", xPortGetCoreID(), host, port, useTLS ? 1 : 0, connectTimeout); 194 | return true; 195 | } 196 | 197 | bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long timeoutMS, String &result, bool &connectionClose) 198 | { 199 | contentLength = 0; 200 | bool start = true; 201 | connectionClose = false; 202 | bool pngFound = false; 203 | 204 | uint32_t headerTimeout = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS; 205 | 206 | while ((currentIsTLS ? secureClient.connected() : client.connected())) 207 | { 208 | if (!readLineWithTimeout(headerTimeout)) 209 | { 210 | result = "Header error or timeout"; 211 | return false; 212 | } 213 | 214 | log_d("read header: %s", headerLine); 215 | 216 | if (start) 217 | { 218 | if (strncmp(headerLine, "HTTP/1.", 7) != 0) 219 | { 220 | result = "Bad HTTP response: "; 221 | result += headerLine; 222 | return false; 223 | } 224 | 225 | // parse status code 226 | int statusCode = 0; 227 | const char *reasonPhrase = ""; 228 | const char *sp1 = strchr(headerLine, ' '); 229 | if (sp1) 230 | { 231 | const char *p = sp1 + 1; 232 | while (*p && isspace((unsigned char)*p)) 233 | p++; 234 | while (*p && isdigit((unsigned char)*p)) 235 | { 236 | statusCode = statusCode * 10 + (*p - '0'); 237 | p++; 238 | } 239 | if (*p == ' ') 240 | reasonPhrase = p + 1; 241 | } 242 | 243 | if (statusCode != 200) 244 | { 245 | result = "HTTP error "; 246 | result += statusCode; 247 | if (*reasonPhrase) 248 | { 249 | result += " ("; 250 | result += reasonPhrase; 251 | result += ")"; 252 | } 253 | return false; 254 | } 255 | 256 | start = false; 257 | } 258 | 259 | if (headerLine[0] == '\0') // empty line = end of headers 260 | break; 261 | 262 | // parse headers 263 | if (strncasecmp(headerLine, "content-length:", 15) == 0) 264 | { 265 | const char *val = headerLine + 15; 266 | while (*val == ' ' || *val == '\t') 267 | val++; 268 | contentLength = atoi(val); 269 | } 270 | else if (strncasecmp(headerLine, "connection:", 11) == 0) 271 | { 272 | const char *val = headerLine + 11; 273 | while (*val == ' ' || *val == '\t') 274 | val++; 275 | if (strcasecmp(val, "close") == 0) 276 | connectionClose = true; 277 | } 278 | else if (strncasecmp(headerLine, "content-type:", 13) == 0) 279 | { 280 | const char *val = headerLine + 13; 281 | while (*val == ' ' || *val == '\t') 282 | val++; 283 | if (strcasecmp(val, "image/png") == 0) 284 | pngFound = true; 285 | } 286 | } 287 | 288 | if (!pngFound) 289 | { 290 | result = "Content-Type not PNG"; 291 | return false; 292 | } 293 | 294 | return true; 295 | } 296 | 297 | bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeoutMS, String &result) 298 | { 299 | uint8_t *dest = buffer.get(); 300 | size_t readSize = 0; 301 | unsigned long lastReadTime = millis(); 302 | 303 | const unsigned long maxStall = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS; 304 | 305 | if (currentIsTLS) 306 | secureClient.setTimeout(maxStall); 307 | else 308 | client.setTimeout(maxStall); 309 | 310 | while (readSize < contentLength) 311 | { 312 | size_t availableData = currentIsTLS ? secureClient.available() : client.available(); 313 | if (availableData == 0) 314 | { 315 | if (millis() - lastReadTime >= maxStall) 316 | { 317 | result = "Timeout: body read stalled for "; 318 | result += maxStall; 319 | result += " ms"; 320 | disconnect(); 321 | return false; 322 | } 323 | taskYIELD(); 324 | continue; 325 | } 326 | 327 | size_t remaining = contentLength - readSize; 328 | size_t toRead = std::min(availableData, remaining); 329 | 330 | int bytesRead = currentIsTLS 331 | ? secureClient.readBytes(dest + readSize, toRead) 332 | : client.readBytes(dest + readSize, toRead); 333 | 334 | if (bytesRead > 0) 335 | { 336 | readSize += bytesRead; 337 | lastReadTime = millis(); 338 | } 339 | else 340 | taskYIELD(); 341 | } 342 | return true; 343 | } 344 | 345 | bool ReusableTileFetcher::readLineWithTimeout(uint32_t timeoutMs) 346 | { 347 | size_t len = 0; 348 | const uint32_t start = millis(); 349 | bool skipping = false; 350 | 351 | while ((millis() - start) < timeoutMs) 352 | { 353 | int availableData = currentIsTLS ? secureClient.available() : client.available(); 354 | if (availableData) 355 | { 356 | char c = currentIsTLS ? secureClient.read() : client.read(); 357 | if (c == '\r') 358 | continue; 359 | 360 | if (c == '\n') 361 | { 362 | if (skipping) 363 | { 364 | // We were discarding an oversized line → reset and keep going 365 | len = 0; 366 | skipping = false; 367 | continue; // stay in loop, keep reading next line 368 | } 369 | 370 | headerLine[len] = '\0'; 371 | return true; // got a usable line 372 | } 373 | 374 | if (!skipping) 375 | { 376 | if (len < sizeof(headerLine) - 1) 377 | { 378 | headerLine[len++] = c; 379 | } 380 | else 381 | { 382 | // buffer too small → switch to skipping mode 383 | skipping = true; 384 | len = 0; // clear partial junk 385 | } 386 | } 387 | } 388 | else 389 | taskYIELD(); 390 | } 391 | 392 | return false; // timeout 393 | } 394 | -------------------------------------------------------------------------------- /src/OpenStreetMap-esp32.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | SPDX-License-Identifier: MIT 22 | */ 23 | 24 | #include "OpenStreetMap-esp32.hpp" 25 | 26 | OpenStreetMap::~OpenStreetMap() 27 | { 28 | if (jobQueue && tasksStarted) 29 | { 30 | constexpr TileJob poison = {0, 0, 255, nullptr}; 31 | for (int i = 0; i < numberOfWorkers; ++i) 32 | if (xQueueSend(jobQueue, &poison, portMAX_DELAY) != pdPASS) 33 | log_e("Failed to send poison pill to tile worker %d", i); 34 | 35 | for (int i = 0; i < numberOfWorkers; ++i) 36 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 37 | 38 | ownerTask = nullptr; 39 | tasksStarted = false; 40 | numberOfWorkers = 0; 41 | 42 | vQueueDelete(jobQueue); 43 | jobQueue = nullptr; 44 | } 45 | 46 | freeTilesCache(); 47 | 48 | if (pngCore0) 49 | { 50 | pngCore0->~PNG(); 51 | heap_caps_free(pngCore0); 52 | pngCore0 = nullptr; 53 | } 54 | if (pngCore1) 55 | { 56 | pngCore1->~PNG(); 57 | heap_caps_free(pngCore1); 58 | pngCore1 = nullptr; 59 | } 60 | } 61 | 62 | void OpenStreetMap::setSize(uint16_t w, uint16_t h) 63 | { 64 | mapWidth = w; 65 | mapHeight = h; 66 | } 67 | 68 | double OpenStreetMap::lon2tile(double lon, uint8_t zoom) 69 | { 70 | return (lon + 180.0) / 360.0 * (1 << zoom); 71 | } 72 | 73 | double OpenStreetMap::lat2tile(double lat, uint8_t zoom) 74 | { 75 | double latRad = lat * M_PI / 180.0; 76 | return (1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / M_PI) / 2.0 * (1 << zoom); 77 | } 78 | 79 | void OpenStreetMap::computeRequiredTiles(double longitude, double latitude, uint8_t zoom, tileList &requiredTiles) 80 | { 81 | // Compute exact tile coordinates 82 | const double exactTileX = lon2tile(longitude, zoom); 83 | const double exactTileY = lat2tile(latitude, zoom); 84 | 85 | // Determine the integer tile indices 86 | const int32_t targetTileX = static_cast(exactTileX); 87 | const int32_t targetTileY = static_cast(exactTileY); 88 | 89 | // Compute the offset inside the tile for the given coordinates 90 | const int16_t targetOffsetX = (exactTileX - targetTileX) * currentProvider->tileSize; 91 | const int16_t targetOffsetY = (exactTileY - targetTileY) * currentProvider->tileSize; 92 | 93 | // Compute the offset for tiles covering the map area to keep the location centered 94 | const int16_t tilesOffsetX = mapWidth / 2 - targetOffsetX; 95 | const int16_t tilesOffsetY = mapHeight / 2 - targetOffsetY; 96 | 97 | // Compute number of colums required 98 | const float colsLeft = 1.0 * tilesOffsetX / currentProvider->tileSize; 99 | const float colsRight = float(mapWidth - (tilesOffsetX + currentProvider->tileSize)) / currentProvider->tileSize; 100 | numberOfColums = ceil(colsLeft) + 1 + ceil(colsRight); 101 | 102 | startOffsetX = tilesOffsetX - (ceil(colsLeft) * currentProvider->tileSize); 103 | 104 | // Compute number of rows required 105 | const float rowsTop = 1.0 * tilesOffsetY / currentProvider->tileSize; 106 | const float rowsBottom = float(mapHeight - (tilesOffsetY + currentProvider->tileSize)) / currentProvider->tileSize; 107 | const uint32_t numberOfRows = ceil(rowsTop) + 1 + ceil(rowsBottom); 108 | 109 | startOffsetY = tilesOffsetY - (ceil(rowsTop) * currentProvider->tileSize); 110 | 111 | log_v(" Need %i * %i tiles. First tile offset is %d,%d", 112 | numberOfColums, numberOfRows, startOffsetX, startOffsetY); 113 | 114 | startTileIndexX = targetTileX - ceil(colsLeft); 115 | startTileIndexY = targetTileY - ceil(rowsTop); 116 | 117 | log_v("top left tile indices: %d, %d", startTileIndexX, startTileIndexY); 118 | 119 | const int32_t worldTileWidth = 1 << zoom; 120 | for (int32_t y = 0; y < numberOfRows; ++y) 121 | { 122 | for (int32_t x = 0; x < numberOfColums; ++x) 123 | { 124 | int32_t tileX = startTileIndexX + x; 125 | const int32_t tileY = startTileIndexY + y; 126 | 127 | // Apply modulo wrapping for tileX 128 | // see https://godbolt.org/z/96e1x7j7r 129 | tileX = (tileX % worldTileWidth + worldTileWidth) % worldTileWidth; 130 | requiredTiles.emplace_back(tileX, tileY); 131 | } 132 | } 133 | } 134 | 135 | CachedTile *OpenStreetMap::findUnusedTile(const tileList &requiredTiles, uint8_t zoom) 136 | { 137 | for (auto &tile : tilesCache) 138 | { 139 | if (tile.busy) 140 | continue; 141 | 142 | // If a tile is valid but not required in the current frame, we can replace it 143 | bool needed = false; 144 | for (const auto &[x, y] : requiredTiles) 145 | { 146 | if (tile.x == x && tile.y == y && tile.z == zoom && tile.valid) 147 | { 148 | needed = true; 149 | break; 150 | } 151 | } 152 | if (!needed) 153 | { 154 | tile.busy = true; 155 | return &tile; 156 | } 157 | } 158 | 159 | return nullptr; // no unused tile found 160 | } 161 | 162 | CachedTile *OpenStreetMap::isTileCached(uint32_t x, uint32_t y, uint8_t z) 163 | { 164 | for (auto &tile : tilesCache) 165 | { 166 | if (tile.x == x && tile.y == y && tile.z == z && tile.valid) 167 | return &tile; 168 | } 169 | return nullptr; 170 | } 171 | 172 | void OpenStreetMap::freeTilesCache() 173 | { 174 | std::vector().swap(tilesCache); 175 | } 176 | 177 | bool OpenStreetMap::resizeTilesCache(uint16_t numberOfTiles) 178 | { 179 | if (!numberOfTiles) 180 | { 181 | log_e("Invalid cache size: %d", numberOfTiles); 182 | return false; 183 | } 184 | 185 | freeTilesCache(); 186 | tilesCache.resize(numberOfTiles); 187 | 188 | for (auto &tile : tilesCache) 189 | { 190 | if (!tile.allocate(currentProvider->tileSize)) 191 | { 192 | log_e("Tile cache allocation failed!"); 193 | freeTilesCache(); 194 | return false; 195 | } 196 | } 197 | return true; 198 | } 199 | 200 | void OpenStreetMap::updateCache(const tileList &requiredTiles, uint8_t zoom, TileBufferList &tilePointers) 201 | { 202 | [[maybe_unused]] const unsigned long startMS = millis(); 203 | std::vector jobs; 204 | makeJobList(requiredTiles, jobs, zoom, tilePointers); 205 | if (!jobs.empty()) 206 | { 207 | runJobs(jobs); 208 | log_i("Finished %i jobs in %lu ms - %i ms/job", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size()); 209 | } 210 | } 211 | 212 | void OpenStreetMap::makeJobList(const tileList &requiredTiles, std::vector &jobs, uint8_t zoom, TileBufferList &tilePointers) 213 | { 214 | for (const auto &[x, y] : requiredTiles) 215 | { 216 | if (y < 0 || y >= (1 << zoom)) 217 | { 218 | tilePointers.push_back(nullptr); // we need to keep 1:1 grid alignment with requiredTiles for composeMap 219 | continue; 220 | } 221 | 222 | const CachedTile *cachedTile = isTileCached(x, y, zoom); 223 | if (cachedTile) 224 | { 225 | tilePointers.push_back(cachedTile->buffer); 226 | continue; 227 | } 228 | 229 | // Check if this tile is already in the job list 230 | const auto job = std::find_if(jobs.begin(), jobs.end(), [&](const TileJob &job) 231 | { return job.x == x && job.y == static_cast(y) && job.z == zoom; }); 232 | if (job != jobs.end()) 233 | { 234 | tilePointers.push_back(job->tile->buffer); // reuse buffer from already queued job 235 | continue; 236 | } 237 | 238 | CachedTile *tileToReplace = findUnusedTile(requiredTiles, zoom); 239 | if (!tileToReplace) 240 | { 241 | log_e("Cache error, no unused tile found, could not store tile %lu, %i, %u", x, y, zoom); 242 | tilePointers.push_back(nullptr); // again, keep 1:1 aligned 243 | continue; 244 | } 245 | 246 | tilePointers.push_back(tileToReplace->buffer); // store buffer for rendering 247 | jobs.push_back({x, static_cast(y), zoom, tileToReplace}); // queue job 248 | } 249 | } 250 | 251 | void OpenStreetMap::runJobs(const std::vector &jobs) 252 | { 253 | log_d("submitting %i jobs", (int)jobs.size()); 254 | 255 | pendingJobs.store(jobs.size()); 256 | startJobsMS = millis(); 257 | for (const TileJob &job : jobs) 258 | if (xQueueSend(jobQueue, &job, 0) != pdPASS) 259 | { 260 | log_e("Failed to enqueue TileJob"); 261 | --pendingJobs; 262 | } 263 | 264 | while (pendingJobs.load() > 0) 265 | vTaskDelay(pdMS_TO_TICKS(1)); 266 | } 267 | 268 | bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePointers) 269 | { 270 | if (mapSprite.width() != mapWidth || mapSprite.height() != mapHeight) 271 | { 272 | mapSprite.deleteSprite(); 273 | mapSprite.setPsram(true); 274 | mapSprite.setColorDepth(lgfx::rgb565_2Byte); 275 | mapSprite.createSprite(mapWidth, mapHeight); 276 | if (!mapSprite.getBuffer()) 277 | { 278 | log_e("could not allocate map"); 279 | return false; 280 | } 281 | } 282 | 283 | for (size_t tileIndex = 0; tileIndex < tilePointers.size(); ++tileIndex) 284 | { 285 | const int drawX = startOffsetX + (tileIndex % numberOfColums) * currentProvider->tileSize; 286 | const int drawY = startOffsetY + (tileIndex / numberOfColums) * currentProvider->tileSize; 287 | const uint16_t *tile = tilePointers[tileIndex]; 288 | if (!tile) 289 | { 290 | mapSprite.fillRect(drawX, drawY, currentProvider->tileSize, currentProvider->tileSize, OSM_BGCOLOR); 291 | continue; 292 | } 293 | mapSprite.pushImage(drawX, drawY, currentProvider->tileSize, currentProvider->tileSize, tile); 294 | } 295 | 296 | mapSprite.setTextColor(TFT_WHITE, OSM_BGCOLOR); 297 | mapSprite.drawRightString(currentProvider->attribution, mapSprite.width(), mapSprite.height() - 10, &DejaVu9Modded); 298 | mapSprite.setTextColor(TFT_WHITE, TFT_BLACK); 299 | return true; 300 | } 301 | 302 | bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS) 303 | { 304 | if (!tasksStarted && !startTileWorkerTasks()) 305 | { 306 | log_e("Failed to start tile worker(s)"); 307 | return false; 308 | } 309 | 310 | if (zoom < currentProvider->minZoom || zoom > currentProvider->maxZoom) 311 | { 312 | log_e("Invalid zoom level: %d", zoom); 313 | return false; 314 | } 315 | 316 | if (!mapWidth || !mapHeight) 317 | { 318 | log_e("Invalid map dimension"); 319 | return false; 320 | } 321 | 322 | if (!tilesCache.capacity() && !resizeTilesCache(tilesNeeded(mapWidth, mapHeight))) 323 | { 324 | log_e("Could not allocate tile cache"); 325 | return false; 326 | } 327 | 328 | // Web Mercator projection only supports latitudes up to ~85.0511°. 329 | // See https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas 330 | // We use 85.0° as a safe and simple boundary. 331 | constexpr double MAX_MERCATOR_LAT = 85.0; 332 | 333 | longitude = fmod(longitude + 180.0, 360.0) - 180.0; 334 | latitude = std::clamp(latitude, -MAX_MERCATOR_LAT, MAX_MERCATOR_LAT); 335 | 336 | tileList requiredTiles; 337 | computeRequiredTiles(longitude, latitude, zoom, requiredTiles); 338 | if (tilesCache.capacity() < requiredTiles.size()) 339 | { 340 | log_e("Caching error: Need %i cache slots, but only %i are provided", requiredTiles.size(), tilesCache.capacity()); 341 | return false; 342 | } 343 | 344 | mapTimeoutMS = timeoutMS; 345 | TileBufferList tilePointers; 346 | updateCache(requiredTiles, zoom, tilePointers); 347 | if (!composeMap(mapSprite, tilePointers)) 348 | { 349 | log_e("Failed to compose map"); 350 | return false; 351 | } 352 | return true; 353 | } 354 | 355 | void OpenStreetMap::PNGDraw(PNGDRAW *pDraw) 356 | { 357 | uint16_t *destRow = currentInstance->currentTileBuffer + (pDraw->y * currentInstance->currentProvider->tileSize); 358 | getPNGCurrentCore()->getLineAsRGB565(pDraw, destRow, PNG_RGB565_BIG_ENDIAN, 0xffffffff); 359 | } 360 | 361 | bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeout) 362 | { 363 | char url[256]; 364 | if (currentProvider->requiresApiKey) 365 | { 366 | snprintf(url, sizeof(url), 367 | currentProvider->urlTemplate, 368 | zoom, x, y, currentProvider->apiKey); 369 | } 370 | else 371 | { 372 | snprintf(url, sizeof(url), 373 | currentProvider->urlTemplate, 374 | zoom, x, y); 375 | } 376 | 377 | MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, timeout); 378 | if (!buffer.isAllocated()) 379 | return false; 380 | 381 | [[maybe_unused]] const unsigned long startMS = millis(); 382 | 383 | PNG *png = getPNGCurrentCore(); 384 | const int16_t rc = png->openRAM(buffer.get(), buffer.size(), PNGDraw); 385 | if (rc != PNG_SUCCESS) 386 | { 387 | result = "PNG Decoder Error: " + String(rc); 388 | return false; 389 | } 390 | 391 | if (png->getWidth() != currentProvider->tileSize || png->getHeight() != currentProvider->tileSize) 392 | { 393 | result = "Unexpected tile size: w=" + String(png->getWidth()) + " h=" + String(png->getHeight()); 394 | return false; 395 | } 396 | 397 | currentInstance = this; 398 | currentTileBuffer = tile.buffer; 399 | const int decodeResult = png->decode(0, PNG_FAST_PALETTE); 400 | if (decodeResult != PNG_SUCCESS) 401 | { 402 | result = "Decoding " + String(url) + " failed with code: " + String(decodeResult); 403 | return false; 404 | } 405 | 406 | log_d("decoding %s took %lu ms on core %i", url, millis() - startMS, xPortGetCoreID()); 407 | 408 | tile.x = x; 409 | tile.y = y; 410 | tile.z = zoom; 411 | return true; 412 | } 413 | 414 | void OpenStreetMap::tileFetcherTask(void *param) 415 | { 416 | ReusableTileFetcher fetcher; 417 | OpenStreetMap *osm = static_cast(param); 418 | while (true) 419 | { 420 | TileJob job; 421 | xQueueReceive(osm->jobQueue, &job, portMAX_DELAY); 422 | [[maybe_unused]] const unsigned long startMS = millis(); 423 | 424 | if (job.z == 255) 425 | break; 426 | 427 | const uint32_t elapsedMS = millis() - osm->startJobsMS; 428 | if (osm->mapTimeoutMS && elapsedMS >= osm->mapTimeoutMS) 429 | { 430 | log_w("Map timeout (%lu ms) exceeded after %lu ms, dropping job", 431 | osm->mapTimeoutMS, elapsedMS); 432 | 433 | osm->invalidateTile(job.tile); 434 | --osm->pendingJobs; 435 | continue; 436 | } 437 | 438 | uint32_t remainingMS = 0; 439 | if (osm->mapTimeoutMS > 0) 440 | { 441 | remainingMS = osm->mapTimeoutMS - elapsedMS; 442 | if (remainingMS == 0) 443 | { 444 | log_w("No budget left for job, dropping"); 445 | osm->invalidateTile(job.tile); 446 | --osm->pendingJobs; 447 | continue; 448 | } 449 | } 450 | 451 | String result; 452 | if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result, remainingMS)) 453 | { 454 | log_e("Tile fetch failed: %s", result.c_str()); 455 | osm->invalidateTile(job.tile); 456 | } 457 | else 458 | { 459 | job.tile->valid = true; 460 | log_d("core %i fetched tile z=%u x=%lu, y=%lu in %lu ms", 461 | xPortGetCoreID(), job.z, job.x, job.y, millis() - startMS); 462 | } 463 | job.tile->busy = false; 464 | --osm->pendingJobs; 465 | } 466 | log_d("task on core %i exiting", xPortGetCoreID()); 467 | xTaskNotifyGive(osm->ownerTask); 468 | vTaskDelete(nullptr); 469 | } 470 | 471 | bool OpenStreetMap::startTileWorkerTasks() 472 | { 473 | if (tasksStarted) 474 | return true; 475 | 476 | if (!jobQueue) 477 | { 478 | jobQueue = xQueueCreate(OSM_JOB_QUEUE_SIZE, sizeof(TileJob)); 479 | if (!jobQueue) 480 | { 481 | log_e("Failed to create job queue!"); 482 | return false; 483 | } 484 | } 485 | 486 | numberOfWorkers = OSM_FORCE_SINGLECORE ? 1 : ESP.getChipCores(); 487 | for (int core = 0; core < numberOfWorkers; ++core) 488 | { 489 | if (!getPNGForCore(core)) 490 | { 491 | log_e("Failed to initialize PNG decoder on core %d", core); 492 | return false; 493 | } 494 | } 495 | 496 | ownerTask = xTaskGetCurrentTaskHandle(); 497 | for (int core = 0; core < numberOfWorkers; ++core) 498 | { 499 | if (!xTaskCreatePinnedToCore(tileFetcherTask, 500 | nullptr, 501 | OSM_TASK_STACKSIZE, 502 | this, 503 | OSM_TASK_PRIORITY, 504 | nullptr, 505 | OSM_FORCE_SINGLECORE ? OSM_SINGLECORE_NUMBER : core)) 506 | { 507 | log_e("Failed to create tile fetcher task on core %d", core); 508 | return false; 509 | } 510 | } 511 | 512 | tasksStarted = true; 513 | 514 | log_i("Started %d tile worker task(s)", numberOfWorkers); 515 | return true; 516 | } 517 | 518 | uint16_t OpenStreetMap::tilesNeeded(uint16_t mapWidth, uint16_t mapHeight) 519 | { 520 | const int tileSize = currentProvider->tileSize; 521 | int tilesX = (mapWidth + tileSize - 1) / tileSize + 1; 522 | int tilesY = (mapHeight + tileSize - 1) / tileSize + 1; 523 | return tilesX * tilesY; 524 | } 525 | 526 | bool OpenStreetMap::setTileProvider(int index) 527 | { 528 | if (index < 0 || index >= OSM_TILEPROVIDERS) 529 | { 530 | log_e("invalid provider index"); 531 | return false; 532 | } 533 | 534 | currentProvider = &tileProviders[index]; 535 | freeTilesCache(); 536 | log_i("provider changed to '%s'", currentProvider->name); 537 | return true; 538 | } 539 | 540 | void OpenStreetMap::invalidateTile(CachedTile *tile) 541 | { 542 | if (!tile) 543 | return; 544 | 545 | const size_t tileByteCount = currentProvider->tileSize * currentProvider->tileSize * 2; 546 | memset(tile->buffer, 0, tileByteCount); 547 | 548 | tile->valid = false; 549 | tile->busy = false; 550 | } 551 | --------------------------------------------------------------------------------