├── CMakeLists.txt ├── .gitignore ├── idf_component.yml ├── LICENSE ├── script ├── README.md └── lvgl_map_tile_converter.py ├── include └── map_tiles.h ├── examples └── basic_map_display.c ├── README.md └── map_tiles.cpp /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register( 2 | SRCS "map_tiles.cpp" 3 | INCLUDE_DIRS "include" 4 | REQUIRES lvgl esp_system 5 | PRIV_REQUIRES vfs fatfs 6 | ) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Build artifacts 7 | build/ 8 | *.o 9 | *.a 10 | *.so 11 | 12 | # Editor files 13 | .vscode/ 14 | .idea/ 15 | *.swp 16 | *.swo 17 | *~ 18 | 19 | # Python 20 | __pycache__/ 21 | *.pyc 22 | *.pyo 23 | *.pyd 24 | .Python 25 | *.egg-info/ 26 | dist/ 27 | -------------------------------------------------------------------------------- /idf_component.yml: -------------------------------------------------------------------------------- 1 | namespace: "0015" 2 | name: "map_tiles" 3 | version: "1.3.0" 4 | maintainers: 5 | - Eric Nam 6 | description: "Map tiles component for LVGL 9.x - Load and display map tiles with GPS coordinate conversion" 7 | url: "https://github.com/0015/map_tiles" 8 | license: "MIT" 9 | 10 | dependencies: 11 | lvgl/lvgl: ">=9.3.0" 12 | idf: ">=5.0" 13 | 14 | targets: 15 | - esp32 16 | - esp32s2 17 | - esp32s3 18 | - esp32c3 19 | - esp32c6 20 | - esp32h2 21 | - esp32p4 22 | 23 | keywords: 24 | - lvgl 25 | - map 26 | - tiles 27 | - gps 28 | - graphics 29 | - ui 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Eric 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. 22 | -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | # **LVGL Map Tile Converter** 2 | 3 | This Python script is designed to convert a directory of PNG map tiles into a format compatible with LVGL (Light and Versatile Graphics Library) version 9\. The output is a series of binary files (.bin) with a header and pixel data in the RGB565 format, optimized for direct use with LVGL's image APIs. 4 | 5 | The script is multithreaded and can significantly speed up the conversion process for large tile sets. 6 | 7 | ## **Features** 8 | 9 | * **PNG to RGB565 Conversion**: Converts standard 24-bit PNG images into 16-bit RGB565. 10 | * **LVGL v9 Compatibility**: Generates a .bin file with the correct LVGL v9 header format. 11 | * **Multithreaded Conversion**: Utilizes a ThreadPoolExecutor to process tiles in parallel, configurable with the \--jobs flag. 12 | * **Directory Traversal**: Automatically finds and converts all .png files within a specified input directory structure. 13 | * **Skip Existing Files**: Skips conversion for tiles that already exist in the output directory unless the \--force flag is used. 14 | 15 | ## **Requirements** 16 | 17 | The script requires the Pillow library to handle image processing. You can install it using pip: 18 | ```bash 19 | pip install Pillow 20 | ``` 21 | 22 | ## **Usage** 23 | 24 | The script is a command-line tool. You can run it from your terminal with the following arguments: 25 | ```bash 26 | python lvgl_map_tile_converter.py --input --output [options] 27 | ``` 28 | 29 | ### **Arguments** 30 | 31 | * \-i, \--input: **Required**. The root folder containing your map tiles. The script expects a tile structure like zoom/x/y.png. 32 | * \-o, \--output: **Required**. The root folder where the converted .bin tiles will be saved. The output structure will mirror the input: zoom/x/y.bin. 33 | * \-j, \--jobs: **Optional**. The number of worker threads to use for the conversion. Defaults to the number of CPU cores on your system. Using more jobs can speed up the process. 34 | * \-f, \--force: **Optional**. If this flag is set, the script will re-convert all tiles, even if the output .bin files already exist. 35 | 36 | ### **Examples** 37 | 38 | **1\. Basic conversion with default settings:** 39 | ```bash 40 | python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 41 | ``` 42 | **2\. Conversion using a specific number of threads (e.g., 8):** 43 | ```bash 44 | python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --jobs 8 45 | ``` 46 | **3\. Forcing a full re-conversion:** 47 | ```bash 48 | python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --force 49 | ``` -------------------------------------------------------------------------------- /script/lvgl_map_tile_converter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import struct 3 | import argparse 4 | import threading 5 | from concurrent.futures import ThreadPoolExecutor, as_completed 6 | from PIL import Image # pip install pillow 7 | 8 | # No implicit defaults; these are set from CLI in main() 9 | INPUT_ROOT = None 10 | OUTPUT_ROOT = None 11 | 12 | 13 | # Convert RGB to 16-bit RGB565 14 | def to_rgb565(r, g, b): 15 | return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) 16 | 17 | 18 | # Strip .png or .bin extensions 19 | def clean_tile_name(filename): 20 | name = filename 21 | while True: 22 | name, ext = os.path.splitext(name) 23 | if ext.lower() not in [".png", ".bin", ".jpg", ".jpeg"]: 24 | break 25 | filename = name 26 | return filename 27 | 28 | 29 | # Create LVGL v9-compatible .bin image 30 | def make_lvgl_bin(png_path, bin_path): 31 | im = Image.open(png_path).convert("RGB") 32 | w, h = im.size 33 | pixels = im.load() 34 | 35 | stride = (w * 16 + 7) // 8 # bytes per row (RGB565 = 16 bpp) 36 | flags = 0x00 # no compression, no premult 37 | color_format = 0x12 # RGB565 38 | magic = 0x19 39 | 40 | header = bytearray() 41 | header += struct.pack("=1) 96 | - force: if True, re-generate even if output exists 97 | """ 98 | if not os.path.isdir(INPUT_ROOT): 99 | print(f"[ERROR] '{INPUT_ROOT}' not found.") 100 | return 101 | 102 | # Build task list (skip existing unless --force) 103 | tasks = [] 104 | for input_path, output_path in _iter_tile_paths(): 105 | if not force and os.path.isfile(output_path): 106 | print(f"[Skip] {output_path}") 107 | continue 108 | tasks.append((input_path, output_path)) 109 | 110 | if not tasks: 111 | print("[INFO] Nothing to do.") 112 | return 113 | 114 | print(f"[INFO] Converting {len(tasks)} tiles with {jobs} thread(s)...") 115 | 116 | if jobs <= 1: 117 | # Serial path 118 | for inp, outp in tasks: 119 | try: 120 | make_lvgl_bin(inp, outp) 121 | except Exception as e: 122 | print(f"[Error] Failed to convert {inp} → {e}") 123 | return 124 | 125 | # Threaded path 126 | print_lock = threading.Lock() 127 | with ThreadPoolExecutor(max_workers=jobs) as ex: 128 | future_map = {ex.submit(make_lvgl_bin, inp, outp): (inp, outp) for inp, outp in tasks} 129 | for fut in as_completed(future_map): 130 | inp, outp = future_map[fut] 131 | try: 132 | fut.result() 133 | except Exception as e: 134 | with print_lock: 135 | print(f"[Error] Failed to convert {inp} → {e}") 136 | 137 | 138 | if __name__ == "__main__": 139 | parser = argparse.ArgumentParser( 140 | description="Convert OSM PNG tiles into LVGL-friendly .bin files (RGB565).", 141 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 142 | ) 143 | parser.add_argument( 144 | "-i", "--input", 145 | required=True, 146 | default=argparse.SUPPRESS, # hide '(default: None)' 147 | help="Input root folder containing tiles in zoom/x/y.png structure", 148 | ) 149 | parser.add_argument( 150 | "-o", "--output", 151 | required=True, 152 | default=argparse.SUPPRESS, # hide '(default: None)' 153 | help="Output root folder where .bin tiles will be written", 154 | ) 155 | parser.add_argument( 156 | "-j", "--jobs", 157 | type=int, 158 | default=os.cpu_count(), 159 | help="Number of worker threads", 160 | ) 161 | parser.add_argument( 162 | "-f", "--force", 163 | action="store_true", 164 | help="Rebuild even if output file already exists", 165 | ) 166 | 167 | args = parser.parse_args() 168 | 169 | # Basic checks 170 | if not os.path.isdir(args.input): 171 | parser.error(f"Input folder not found or not a directory: {args.input}") 172 | os.makedirs(args.output, exist_ok=True) 173 | 174 | # Apply CLI values 175 | INPUT_ROOT = args.input 176 | OUTPUT_ROOT = args.output 177 | 178 | convert_all_tiles(jobs=max(1, args.jobs), force=args.force) 179 | -------------------------------------------------------------------------------- /include/map_tiles.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "lvgl.h" 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | /** 12 | * @brief Map tiles component for LVGL 9.x 13 | * 14 | * This component provides functionality to load and display map tiles with GPS coordinate conversion. 15 | * Tiles are assumed to be 256x256 pixels in RGB565 format, stored in binary files. 16 | */ 17 | 18 | // Constants 19 | #define MAP_TILES_TILE_SIZE 256 20 | #define MAP_TILES_DEFAULT_GRID_COLS 5 21 | #define MAP_TILES_DEFAULT_GRID_ROWS 5 22 | #define MAP_TILES_MAX_GRID_COLS 9 23 | #define MAP_TILES_MAX_GRID_ROWS 9 24 | #define MAP_TILES_MAX_TILES (MAP_TILES_MAX_GRID_COLS * MAP_TILES_MAX_GRID_ROWS) 25 | #define MAP_TILES_BYTES_PER_PIXEL 2 26 | #define MAP_TILES_COLOR_FORMAT LV_COLOR_FORMAT_RGB565 27 | #define MAP_TILES_MAX_TYPES 8 28 | #define MAP_TILES_MAX_FOLDER_NAME 32 29 | 30 | /** 31 | * @brief Configuration structure for map tiles 32 | */ 33 | typedef struct { 34 | const char* base_path; /**< Base path where tiles are stored (e.g., "/sdcard") */ 35 | const char* tile_folders[MAP_TILES_MAX_TYPES]; /**< Array of folder names for different tile types */ 36 | int tile_type_count; /**< Number of tile types configured */ 37 | int grid_cols; /**< Number of tile columns (default: 5, max: 9) */ 38 | int grid_rows; /**< Number of tile rows (default: 5, max: 9) */ 39 | int default_zoom; /**< Default zoom level */ 40 | bool use_spiram; /**< Whether to use SPIRAM for tile buffers */ 41 | int default_tile_type; /**< Default tile type index (0 to tile_type_count-1) */ 42 | } map_tiles_config_t; 43 | 44 | /** 45 | * @brief Map tiles handle 46 | */ 47 | typedef struct map_tiles_t* map_tiles_handle_t; 48 | 49 | /** 50 | * @brief Initialize the map tiles system 51 | * 52 | * @param config Configuration structure 53 | * @return map_tiles_handle_t Handle to the map tiles instance, NULL on failure 54 | */ 55 | map_tiles_handle_t map_tiles_init(const map_tiles_config_t* config); 56 | 57 | /** 58 | * @brief Set the zoom level 59 | * 60 | * @param handle Map tiles handle 61 | * @param zoom_level Zoom level (typically 0-18) 62 | */ 63 | void map_tiles_set_zoom(map_tiles_handle_t handle, int zoom_level); 64 | 65 | /** 66 | * @brief Get the current zoom level 67 | * 68 | * @param handle Map tiles handle 69 | * @return Current zoom level 70 | */ 71 | int map_tiles_get_zoom(map_tiles_handle_t handle); 72 | 73 | /** 74 | * @brief Set tile type 75 | * 76 | * @param handle Map tiles handle 77 | * @param tile_type Tile type index (0 to configured tile_type_count-1) 78 | * @return true if tile type was set successfully, false otherwise 79 | */ 80 | bool map_tiles_set_tile_type(map_tiles_handle_t handle, int tile_type); 81 | 82 | /** 83 | * @brief Get current tile type 84 | * 85 | * @param handle Map tiles handle 86 | * @return Current tile type index, -1 if error 87 | */ 88 | int map_tiles_get_tile_type(map_tiles_handle_t handle); 89 | 90 | /** 91 | * @brief Get grid dimensions 92 | * 93 | * @param handle Map tiles handle 94 | * @param cols Output grid columns (can be NULL) 95 | * @param rows Output grid rows (can be NULL) 96 | */ 97 | void map_tiles_get_grid_size(map_tiles_handle_t handle, int* cols, int* rows); 98 | 99 | /** 100 | * @brief Get total tile count for current grid 101 | * 102 | * @param handle Map tiles handle 103 | * @return Total number of tiles in the grid, 0 if error 104 | */ 105 | int map_tiles_get_tile_count(map_tiles_handle_t handle); 106 | 107 | /** 108 | * @brief Get tile type count 109 | * 110 | * @param handle Map tiles handle 111 | * @return Number of configured tile types, 0 if error 112 | */ 113 | int map_tiles_get_tile_type_count(map_tiles_handle_t handle); 114 | 115 | /** 116 | * @brief Get tile type folder name 117 | * 118 | * @param handle Map tiles handle 119 | * @param tile_type Tile type index 120 | * @return Folder name for the tile type, NULL if invalid 121 | */ 122 | const char* map_tiles_get_tile_type_folder(map_tiles_handle_t handle, int tile_type); 123 | 124 | /** 125 | * @brief Load a specific tile dynamically 126 | * 127 | * @param handle Map tiles handle 128 | * @param index Tile index (0 to total_tile_count-1) 129 | * @param tile_x Tile X coordinate 130 | * @param tile_y Tile Y coordinate 131 | * @return true if tile loaded successfully, false otherwise 132 | */ 133 | bool map_tiles_load_tile(map_tiles_handle_t handle, int index, int tile_x, int tile_y); 134 | 135 | /** 136 | * @brief Convert GPS coordinates to tile coordinates 137 | * 138 | * @param handle Map tiles handle 139 | * @param lat Latitude in degrees 140 | * @param lon Longitude in degrees 141 | * @param x Output tile X coordinate 142 | * @param y Output tile Y coordinate 143 | */ 144 | void map_tiles_gps_to_tile_xy(map_tiles_handle_t handle, double lat, double lon, double* x, double* y); 145 | 146 | /** 147 | * @brief Convert tile coordinates to GPS coordinates 148 | * 149 | * @param handle Map tiles handle 150 | * @param x Tile X coordinate 151 | * @param y Tile Y coordinate 152 | * @param lat Output latitude in degrees 153 | * @param lon Output longitude in degrees 154 | */ 155 | void map_tiles_tile_xy_to_gps(map_tiles_handle_t handle, double x, double y, double* lat, double* lon); 156 | 157 | /** 158 | * @brief Get center GPS coordinates of current map view 159 | * 160 | * @param handle Map tiles handle 161 | * @param lat Output latitude in degrees 162 | * @param lon Output longitude in degrees 163 | */ 164 | void map_tiles_get_center_gps(map_tiles_handle_t handle, double* lat, double* lon); 165 | 166 | /** 167 | * @brief Set the tile center from GPS coordinates 168 | * 169 | * @param handle Map tiles handle 170 | * @param lat Latitude in degrees 171 | * @param lon Longitude in degrees 172 | */ 173 | void map_tiles_set_center_from_gps(map_tiles_handle_t handle, double lat, double lon); 174 | 175 | /** 176 | * @brief Check if GPS coordinates are within current tile grid 177 | * 178 | * @param handle Map tiles handle 179 | * @param lat Latitude in degrees 180 | * @param lon Longitude in degrees 181 | * @return true if GPS position is within current tiles, false otherwise 182 | */ 183 | bool map_tiles_is_gps_within_tiles(map_tiles_handle_t handle, double lat, double lon); 184 | 185 | /** 186 | * @brief Get current tile position 187 | * 188 | * @param handle Map tiles handle 189 | * @param tile_x Output tile X coordinate (can be NULL) 190 | * @param tile_y Output tile Y coordinate (can be NULL) 191 | */ 192 | void map_tiles_get_position(map_tiles_handle_t handle, int* tile_x, int* tile_y); 193 | 194 | /** 195 | * @brief Set tile position 196 | * 197 | * @param handle Map tiles handle 198 | * @param tile_x Tile X coordinate 199 | * @param tile_y Tile Y coordinate 200 | */ 201 | void map_tiles_set_position(map_tiles_handle_t handle, int tile_x, int tile_y); 202 | 203 | /** 204 | * @brief Get marker offset within the current tile 205 | * 206 | * @param handle Map tiles handle 207 | * @param offset_x Output X offset in pixels (can be NULL) 208 | * @param offset_y Output Y offset in pixels (can be NULL) 209 | */ 210 | void map_tiles_get_marker_offset(map_tiles_handle_t handle, int* offset_x, int* offset_y); 211 | 212 | /** 213 | * @brief Set marker offset within the current tile 214 | * 215 | * @param handle Map tiles handle 216 | * @param offset_x X offset in pixels 217 | * @param offset_y Y offset in pixels 218 | */ 219 | void map_tiles_set_marker_offset(map_tiles_handle_t handle, int offset_x, int offset_y); 220 | 221 | /** 222 | * @brief Get tile image descriptor 223 | * 224 | * @param handle Map tiles handle 225 | * @param index Tile index (0 to total_tile_count-1) 226 | * @return Pointer to LVGL image descriptor, NULL if invalid 227 | */ 228 | lv_image_dsc_t* map_tiles_get_image(map_tiles_handle_t handle, int index); 229 | 230 | /** 231 | * @brief Get tile buffer 232 | * 233 | * @param handle Map tiles handle 234 | * @param index Tile index (0 to total_tile_count-1) 235 | * @return Pointer to tile buffer, NULL if invalid 236 | */ 237 | uint8_t* map_tiles_get_buffer(map_tiles_handle_t handle, int index); 238 | 239 | /** 240 | * @brief Set tile loading error state 241 | * 242 | * @param handle Map tiles handle 243 | * @param error Error state 244 | */ 245 | void map_tiles_set_loading_error(map_tiles_handle_t handle, bool error); 246 | 247 | /** 248 | * @brief Check if there's a tile loading error 249 | * 250 | * @param handle Map tiles handle 251 | * @return true if there's an error, false otherwise 252 | */ 253 | bool map_tiles_has_loading_error(map_tiles_handle_t handle); 254 | 255 | /** 256 | * @brief Clean up and free map tiles resources 257 | * 258 | * @param handle Map tiles handle 259 | */ 260 | void map_tiles_cleanup(map_tiles_handle_t handle); 261 | 262 | #ifdef __cplusplus 263 | } 264 | #endif 265 | -------------------------------------------------------------------------------- /examples/basic_map_display.c: -------------------------------------------------------------------------------- 1 | #include "map_tiles.h" 2 | #include "lvgl.h" 3 | #include "esp_log.h" 4 | 5 | static const char* TAG = "map_example"; 6 | 7 | // Map tiles handle 8 | static map_tiles_handle_t map_handle = NULL; 9 | 10 | // LVGL objects for displaying tiles 11 | static lv_obj_t* map_container = NULL; 12 | static lv_obj_t** tile_images = NULL; // Dynamic array for configurable grid 13 | static int grid_cols = 0, grid_rows = 0, tile_count = 0; 14 | 15 | /** 16 | * @brief Initialize the map display 17 | */ 18 | void map_display_init(void) 19 | { 20 | // Configure map tiles with multiple tile types and custom grid size 21 | const char* tile_folders[] = {"street_map", "satellite", "terrain", "hybrid"}; 22 | map_tiles_config_t config = { 23 | .base_path = "/sdcard", 24 | .tile_folders = {tile_folders[0], tile_folders[1], tile_folders[2], tile_folders[3]}, 25 | .tile_type_count = 4, 26 | .default_zoom = 10, 27 | .use_spiram = true, 28 | .default_tile_type = 0, // Start with street map 29 | .grid_cols = 5, // 5x5 grid (configurable) 30 | .grid_rows = 5 31 | }; 32 | 33 | // Initialize map tiles 34 | map_handle = map_tiles_init(&config); 35 | if (!map_handle) { 36 | ESP_LOGE(TAG, "Failed to initialize map tiles"); 37 | return; 38 | } 39 | 40 | // Get grid dimensions 41 | map_tiles_get_grid_size(map_handle, &grid_cols, &grid_rows); 42 | tile_count = map_tiles_get_tile_count(map_handle); 43 | 44 | // Allocate tile images array 45 | tile_images = malloc(tile_count * sizeof(lv_obj_t*)); 46 | if (!tile_images) { 47 | ESP_LOGE(TAG, "Failed to allocate tile images array"); 48 | map_tiles_cleanup(map_handle); 49 | return; 50 | } 51 | 52 | // Create map container 53 | map_container = lv_obj_create(lv_screen_active()); 54 | lv_obj_set_size(map_container, 55 | grid_cols * MAP_TILES_TILE_SIZE, 56 | grid_rows * MAP_TILES_TILE_SIZE); 57 | lv_obj_center(map_container); 58 | lv_obj_set_style_pad_all(map_container, 0, 0); 59 | lv_obj_set_style_border_width(map_container, 0, 0); 60 | 61 | // Create image widgets for each tile 62 | for (int i = 0; i < tile_count; i++) { 63 | tile_images[i] = lv_image_create(map_container); 64 | 65 | // Position tile in grid 66 | int row = i / grid_cols; 67 | int col = i % grid_cols; 68 | lv_obj_set_pos(tile_images[i], 69 | col * MAP_TILES_TILE_SIZE, 70 | row * MAP_TILES_TILE_SIZE); 71 | lv_obj_set_size(tile_images[i], MAP_TILES_TILE_SIZE, MAP_TILES_TILE_SIZE); 72 | } 73 | 74 | ESP_LOGI(TAG, "Map display initialized"); 75 | } 76 | 77 | /** 78 | * @brief Load and display map tiles for a GPS location 79 | * 80 | * @param lat Latitude in degrees 81 | * @param lon Longitude in degrees 82 | */ 83 | void map_display_load_location(double lat, double lon) 84 | { 85 | if (!map_handle) { 86 | ESP_LOGE(TAG, "Map not initialized"); 87 | return; 88 | } 89 | 90 | ESP_LOGI(TAG, "Loading map for GPS: %.6f, %.6f", lat, lon); 91 | 92 | // Set center from GPS coordinates 93 | map_tiles_set_center_from_gps(map_handle, lat, lon); 94 | 95 | // Get current tile position 96 | int base_tile_x, base_tile_y; 97 | map_tiles_get_position(map_handle, &base_tile_x, &base_tile_y); 98 | 99 | // Load tiles in a configurable grid 100 | for (int row = 0; row < grid_rows; row++) { 101 | for (int col = 0; col < grid_cols; col++) { 102 | int index = row * grid_cols + col; 103 | int tile_x = base_tile_x + col; 104 | int tile_y = base_tile_y + row; 105 | 106 | // Load the tile 107 | bool loaded = map_tiles_load_tile(map_handle, index, tile_x, tile_y); 108 | if (loaded) { 109 | // Update the image widget 110 | lv_image_dsc_t* img_dsc = map_tiles_get_image(map_handle, index); 111 | if (img_dsc) { 112 | lv_image_set_src(tile_images[index], img_dsc); 113 | ESP_LOGD(TAG, "Loaded tile %d (%d, %d)", index, tile_x, tile_y); 114 | } 115 | } else { 116 | ESP_LOGW(TAG, "Failed to load tile %d (%d, %d)", index, tile_x, tile_y); 117 | // Set a placeholder or clear the image 118 | lv_image_set_src(tile_images[index], NULL); 119 | } 120 | } 121 | } 122 | 123 | ESP_LOGI(TAG, "Map tiles loaded for location"); 124 | } 125 | 126 | /** 127 | * @brief Set the map tile type and reload tiles 128 | * 129 | * @param tile_type Tile type index (0=street, 1=satellite, 2=terrain, 3=hybrid) 130 | * @param lat Current latitude 131 | * @param lon Current longitude 132 | */ 133 | void map_display_set_tile_type(int tile_type, double lat, double lon) 134 | { 135 | if (!map_handle) { 136 | ESP_LOGE(TAG, "Map not initialized"); 137 | return; 138 | } 139 | 140 | // Validate tile type 141 | int max_types = map_tiles_get_tile_type_count(map_handle); 142 | if (tile_type < 0 || tile_type >= max_types) { 143 | ESP_LOGW(TAG, "Invalid tile type %d (valid range: 0-%d)", tile_type, max_types - 1); 144 | return; 145 | } 146 | 147 | ESP_LOGI(TAG, "Setting tile type to %d (%s)", tile_type, 148 | map_tiles_get_tile_type_folder(map_handle, tile_type)); 149 | 150 | // Set tile type 151 | if (map_tiles_set_tile_type(map_handle, tile_type)) { 152 | // Reload tiles for the new type 153 | map_display_load_location(lat, lon); 154 | } 155 | } 156 | 157 | /** 158 | * @brief Set the zoom level and reload tiles 159 | * 160 | * @param zoom New zoom level 161 | * @param lat Current latitude 162 | * @param lon Current longitude 163 | */ 164 | void map_display_set_zoom(int zoom, double lat, double lon) 165 | { 166 | if (!map_handle) { 167 | ESP_LOGE(TAG, "Map not initialized"); 168 | return; 169 | } 170 | 171 | ESP_LOGI(TAG, "Setting zoom to %d", zoom); 172 | 173 | // Update zoom level 174 | map_tiles_set_zoom(map_handle, zoom); 175 | 176 | // Reload tiles for the new zoom level 177 | map_display_load_location(lat, lon); 178 | } 179 | 180 | /** 181 | * @brief Add a GPS marker to the map 182 | * 183 | * @param lat Latitude in degrees 184 | * @param lon Longitude in degrees 185 | */ 186 | void map_display_add_marker(double lat, double lon) 187 | { 188 | if (!map_handle) { 189 | ESP_LOGE(TAG, "Map not initialized"); 190 | return; 191 | } 192 | 193 | // Check if GPS position is within current tiles 194 | if (!map_tiles_is_gps_within_tiles(map_handle, lat, lon)) { 195 | ESP_LOGW(TAG, "GPS position outside current tiles, reloading map"); 196 | map_display_load_location(lat, lon); 197 | return; 198 | } 199 | 200 | // Convert GPS to tile coordinates 201 | double tile_x, tile_y; 202 | map_tiles_gps_to_tile_xy(map_handle, lat, lon, &tile_x, &tile_y); 203 | 204 | // Get current grid position (top-left tile) 205 | int base_tile_x, base_tile_y; 206 | map_tiles_get_position(map_handle, &base_tile_x, &base_tile_y); 207 | 208 | // Calculate absolute pixel position of marker 209 | int abs_px = (int)(tile_x * MAP_TILES_TILE_SIZE); 210 | int abs_py = (int)(tile_y * MAP_TILES_TILE_SIZE); 211 | 212 | // Calculate top-left pixel position of current tile grid 213 | int top_left_px_x = base_tile_x * MAP_TILES_TILE_SIZE; 214 | int top_left_px_y = base_tile_y * MAP_TILES_TILE_SIZE; 215 | 216 | // Get scroll position if map is scrollable 217 | lv_coord_t scroll_x = lv_obj_get_scroll_x(map_container); 218 | lv_coord_t scroll_y = lv_obj_get_scroll_y(map_container); 219 | 220 | // Calculate marker position relative to current view 221 | int marker_x = abs_px - top_left_px_x - scroll_x - 5; // -5 to center the 10px marker 222 | int marker_y = abs_py - top_left_px_y - scroll_y - 5; 223 | 224 | ESP_LOGI(TAG, "Marker calculation: tile_xy=(%.3f,%.3f) base=(%d,%d) abs_px=(%d,%d) scroll=(%d,%d) pixel=(%d,%d)", 225 | tile_x, tile_y, base_tile_x, base_tile_y, abs_px, abs_py, scroll_x, scroll_y, marker_x, marker_y); 226 | 227 | // Check if marker is within visible bounds 228 | int container_width = grid_cols * MAP_TILES_TILE_SIZE; 229 | int container_height = grid_rows * MAP_TILES_TILE_SIZE; 230 | if (marker_x < -10 || marker_x > container_width || marker_y < -10 || marker_y > container_height) { 231 | ESP_LOGW(TAG, "Marker at (%d, %d) is outside visible bounds (0,0) to (%d,%d)", 232 | marker_x, marker_y, container_width, container_height); 233 | } 234 | 235 | // Create or update marker object 236 | static lv_obj_t* marker = NULL; 237 | if (!marker) { 238 | marker = lv_obj_create(map_container); 239 | lv_obj_set_size(marker, 10, 10); 240 | lv_obj_set_style_bg_color(marker, lv_color_hex(0xFF0000), 0); 241 | lv_obj_set_style_radius(marker, 5, 0); 242 | lv_obj_set_style_border_width(marker, 1, 0); 243 | lv_obj_set_style_border_color(marker, lv_color_hex(0xFFFFFF), 0); 244 | } 245 | 246 | lv_obj_set_pos(marker, marker_x, marker_y); 247 | 248 | ESP_LOGI(TAG, "GPS marker at (%.6f, %.6f) positioned at pixel (%d, %d)", 249 | lat, lon, marker_x, marker_y); 250 | } 251 | 252 | /** 253 | * @brief Clean up map display resources 254 | */ 255 | void map_display_cleanup(void) 256 | { 257 | if (tile_images) { 258 | free(tile_images); 259 | tile_images = NULL; 260 | } 261 | 262 | if (map_handle) { 263 | map_tiles_cleanup(map_handle); 264 | map_handle = NULL; 265 | } 266 | 267 | if (map_container) { 268 | lv_obj_delete(map_container); 269 | map_container = NULL; 270 | } 271 | 272 | grid_cols = grid_rows = tile_count = 0; 273 | 274 | ESP_LOGI(TAG, "Map display cleaned up"); 275 | } 276 | 277 | /** 278 | * @brief Example usage in main application 279 | */ 280 | void app_main(void) 281 | { 282 | // Initialize LVGL and display driver first... 283 | 284 | // Initialize map display 285 | map_display_init(); 286 | 287 | // Load map for San Francisco 288 | double lat = 37.7749; 289 | double lon = -122.4194; 290 | map_display_load_location(lat, lon); 291 | 292 | // Add GPS marker 293 | map_display_add_marker(lat, lon); 294 | 295 | // Example: Change to satellite view (tile type 1) 296 | // map_display_set_tile_type(1, lat, lon); 297 | 298 | // Example: Change to terrain view (tile type 2) 299 | // map_display_set_tile_type(2, lat, lon); 300 | 301 | // Example: Change zoom level 302 | // map_display_set_zoom(12, lat, lon); 303 | 304 | // Example: Update GPS position 305 | // map_display_add_marker(37.7849, -122.4094); 306 | 307 | // NOTE: To use different grid sizes, modify the grid_cols and grid_rows 308 | // in the config structure above. Examples: 309 | // - 3x3 grid: .grid_cols = 3, .grid_rows = 3 (9 tiles, ~1.1MB RAM) 310 | // - 5x5 grid: .grid_cols = 5, .grid_rows = 5 (25 tiles, ~3.1MB RAM) 311 | // - 7x7 grid: .grid_cols = 7, .grid_rows = 7 (49 tiles, ~6.1MB RAM) 312 | } 313 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Map Tiles Component for LVGL 9.x 2 | 3 | A comprehensive map tiles component for ESP-IDF projects using LVGL 9.x. This component provides functionality to load and display map tiles with GPS coordinate conversion, designed for embedded applications requiring offline map display capabilities. 4 | 5 | ## Recent Updates 6 | 7 | **v1.3.0 (December 20, 2025)** 8 | - **Fixed critical marker positioning bugs in example code:** 9 | - Bug Fix #1: Corrected `map_display_add_marker()` to calculate position from actual GPS coordinates instead of using stored offset 10 | - Bug Fix #2: Added scroll position compensation for scrollable map containers 11 | - Markers now appear at correct GPS locations regardless of map center position, zoom level, or scroll position 12 | - Enhanced debug logging with detailed marker calculation information 13 | - Added bounds checking for marker positions with warnings 14 | 15 | ## Features 16 | 17 | - **LVGL 9.x Compatible**: Fully compatible with LVGL 9.x image handling 18 | - **GPS Coordinate Conversion**: Convert GPS coordinates to tile coordinates and vice versa 19 | - **Dynamic Tile Loading**: Load map tiles on demand from file system 20 | - **Configurable Grid Size**: Support for different grid sizes (3x3, 5x5, 7x7, etc.) 21 | - **Multiple Tile Types**: Support for up to 8 different tile types (street, satellite, terrain, hybrid, etc.) 22 | - **Memory Efficient**: Configurable memory allocation (SPIRAM or regular RAM) 23 | - **Multiple Zoom Levels**: Support for different map zoom levels 24 | - **Error Handling**: Comprehensive error handling and logging 25 | - **C API**: Clean C API for easy integration 26 | 27 | ## Requirements 28 | 29 | - ESP-IDF 5.0 or later 30 | - LVGL 9.3 31 | - File system support (FAT/SPIFFS/LittleFS) 32 | - Map tiles in binary format (RGB565, 256x256 pixels) 33 | 34 | ## Installation 35 | 36 | ### Using ESP-IDF Component Manager 37 | 38 | You can easily add this component to your project using the idf.py command or by manually updating your idf_component.yml file. 39 | 40 | #### Option 1: Using the idf.py add-dependency command (Recommended) 41 | From your project's root directory, simply run the following command in your terminal: 42 | 43 | ```bash 44 | idf.py add-dependency "0015/map_tiles^1.3.0" 45 | ``` 46 | 47 | This command will automatically add the component to your idf_component.yml file and download the required files the next time you build your project. 48 | 49 | #### Option 2: Manual idf_component.yml update 50 | Add to your project's `main/idf_component.yml`: 51 | 52 | ```yaml 53 | dependencies: 54 | map_tiles: 55 | git: "https://github.com/0015/map_tiles.git" 56 | version: "^1.3.0" 57 | ``` 58 | 59 | ### Manual Installation 60 | 61 | 1. Copy the `map_tiles` folder to your project's `components` directory 62 | 2. The component will be automatically included in your build 63 | 64 | ## Usage 65 | 66 | ### Basic Setup 67 | 68 | ```c 69 | #include "map_tiles.h" 70 | 71 | // Configure the map tiles with multiple tile types and custom grid size 72 | const char* tile_folders[] = {"street_map", "satellite", "terrain", "hybrid"}; 73 | map_tiles_config_t config = { 74 | .base_path = "/sdcard", // Base path to tile storage 75 | .tile_folders = {tile_folders[0], tile_folders[1], tile_folders[2], tile_folders[3]}, 76 | .tile_type_count = 4, // Number of tile types 77 | .default_zoom = 10, // Default zoom level 78 | .use_spiram = true, // Use SPIRAM if available 79 | .default_tile_type = 0, // Start with street map (index 0) 80 | .grid_cols = 5, // Grid width (tiles) 81 | .grid_rows = 5 // Grid height (tiles) 82 | }; 83 | 84 | // Initialize map tiles 85 | map_tiles_handle_t map_handle = map_tiles_init(&config); 86 | if (!map_handle) { 87 | ESP_LOGE(TAG, "Failed to initialize map tiles"); 88 | return; 89 | } 90 | ``` 91 | 92 | ### Loading Tiles 93 | 94 | ```c 95 | // Set center position from GPS coordinates 96 | map_tiles_set_center_from_gps(map_handle, 37.7749, -122.4194); // San Francisco 97 | 98 | // Get grid dimensions 99 | int grid_cols, grid_rows; 100 | map_tiles_get_grid_size(map_handle, &grid_cols, &grid_rows); 101 | int tile_count = map_tiles_get_tile_count(map_handle); 102 | 103 | // Load tiles for the configured grid size 104 | for (int row = 0; row < grid_rows; row++) { 105 | for (int col = 0; col < grid_cols; col++) { 106 | int index = row * grid_cols + col; 107 | int tile_x, tile_y; 108 | map_tiles_get_position(map_handle, &tile_x, &tile_y); 109 | 110 | bool loaded = map_tiles_load_tile(map_handle, index, 111 | tile_x + col, tile_y + row); 112 | if (!loaded) { 113 | ESP_LOGW(TAG, "Failed to load tile %d", index); 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | ### Displaying Tiles with LVGL 120 | 121 | ```c 122 | // Get grid dimensions and tile count 123 | int grid_cols, grid_rows; 124 | map_tiles_get_grid_size(map_handle, &grid_cols, &grid_rows); 125 | int tile_count = map_tiles_get_tile_count(map_handle); 126 | 127 | // Create image widgets for each tile 128 | lv_obj_t** tile_images = malloc(tile_count * sizeof(lv_obj_t*)); 129 | 130 | for (int i = 0; i < tile_count; i++) { 131 | tile_images[i] = lv_image_create(parent_container); 132 | 133 | // Get the tile image descriptor 134 | lv_image_dsc_t* img_dsc = map_tiles_get_image(map_handle, i); 135 | if (img_dsc) { 136 | lv_image_set_src(tile_images[i], img_dsc); 137 | 138 | // Position the tile in the grid 139 | int row = i / grid_cols; 140 | int col = i % grid_cols; 141 | lv_obj_set_pos(tile_images[i], 142 | col * MAP_TILES_TILE_SIZE, 143 | row * MAP_TILES_TILE_SIZE); 144 | } 145 | } 146 | ``` 147 | 148 | ### Switching Tile Types 149 | 150 | ```c 151 | // Switch to different tile types 152 | map_tiles_set_tile_type(map_handle, 0); // Street map 153 | map_tiles_set_tile_type(map_handle, 1); // Satellite 154 | map_tiles_set_tile_type(map_handle, 2); // Terrain 155 | map_tiles_set_tile_type(map_handle, 3); // Hybrid 156 | 157 | // Get current tile type 158 | int current_type = map_tiles_get_tile_type(map_handle); 159 | 160 | // Get available tile types 161 | int type_count = map_tiles_get_tile_type_count(map_handle); 162 | for (int i = 0; i < type_count; i++) { 163 | const char* folder = map_tiles_get_tile_type_folder(map_handle, i); 164 | printf("Tile type %d: %s\n", i, folder); 165 | } 166 | ``` 167 | 168 | ### GPS Coordinate Conversion 169 | 170 | ```c 171 | // Convert GPS to tile coordinates 172 | double tile_x, tile_y; 173 | map_tiles_gps_to_tile_xy(map_handle, 37.7749, -122.4194, &tile_x, &tile_y); 174 | 175 | // Check if GPS position is within current tile grid 176 | bool within_tiles = map_tiles_is_gps_within_tiles(map_handle, 37.7749, -122.4194); 177 | 178 | // Get marker offset for precise positioning 179 | int offset_x, offset_y; 180 | map_tiles_get_marker_offset(map_handle, &offset_x, &offset_y); 181 | ``` 182 | 183 | ### Memory Management 184 | 185 | ```c 186 | // Clean up when done 187 | map_tiles_cleanup(map_handle); 188 | ``` 189 | 190 | ## Tile File Format 191 | 192 | The component expects map tiles in a specific binary format: 193 | 194 | - **File Structure**: `{base_path}/{map_tile}/{zoom}/{tile_x}/{tile_y}.bin` 195 | - **Format**: 12-byte header + raw RGB565 pixel data 196 | - **Size**: 256x256 pixels 197 | - **Color Format**: RGB565 (16-bit per pixel) 198 | 199 | ### Example Tile Structure 200 | ``` 201 | /sdcard/ 202 | ├── street_map/ // Tile type 0 203 | │ ├── 10/ 204 | │ │ ├── 164/ 205 | │ │ │ ├── 395.bin 206 | │ │ │ ├── 396.bin 207 | │ │ │ └── ... 208 | │ │ └── ... 209 | │ └── ... 210 | ├── satellite/ // Tile type 1 211 | │ ├── 10/ 212 | │ │ ├── 164/ 213 | │ │ │ ├── 395.bin 214 | │ │ │ └── ... 215 | │ │ └── ... 216 | │ └── ... 217 | ├── terrain/ // Tile type 2 218 | │ └── ... 219 | └── hybrid/ // Tile type 3 220 | └── ... 221 | ``` 222 | 223 | ## Configuration Options 224 | 225 | | Parameter | Type | Description | Default | 226 | |-----------|------|-------------|---------| 227 | | `base_path` | `const char*` | Base directory for tile storage | Required | 228 | | `tile_folders` | `const char*[]` | Array of folder names for different tile types | Required | 229 | | `tile_type_count` | `int` | Number of tile types (max 8) | Required | 230 | | `default_zoom` | `int` | Initial zoom level | Required | 231 | | `use_spiram` | `bool` | Use SPIRAM for tile buffers | `false` | 232 | | `default_tile_type` | `int` | Initial tile type index | Required | 233 | | `grid_cols` | `int` | Number of tile columns (max 10) | 5 | 234 | | `grid_rows` | `int` | Number of tile rows (max 10) | 5 | 235 | 236 | ## API Reference 237 | 238 | ### Initialization 239 | - `map_tiles_init()` - Initialize map tiles system 240 | - `map_tiles_cleanup()` - Clean up resources 241 | 242 | ### Tile Management 243 | - `map_tiles_load_tile()` - Load a specific tile 244 | - `map_tiles_get_image()` - Get LVGL image descriptor 245 | - `map_tiles_get_buffer()` - Get raw tile buffer 246 | 247 | ### Grid Management 248 | - `map_tiles_get_grid_size()` - Get current grid dimensions 249 | - `map_tiles_get_tile_count()` - Get total number of tiles in grid 250 | 251 | ### Coordinate Conversion 252 | - `map_tiles_gps_to_tile_xy()` - Convert GPS to tile coordinates 253 | - `map_tiles_set_center_from_gps()` - Set center from GPS 254 | - `map_tiles_is_gps_within_tiles()` - Check if GPS is within current tiles 255 | 256 | ### Position Management 257 | - `map_tiles_get_position()` - Get current tile position 258 | - `map_tiles_set_position()` - Set tile position 259 | - `map_tiles_get_marker_offset()` - Get marker offset 260 | - `map_tiles_set_marker_offset()` - Set marker offset 261 | 262 | ### Tile Type Management 263 | - `map_tiles_set_tile_type()` - Set active tile type 264 | - `map_tiles_get_tile_type()` - Get current tile type 265 | - `map_tiles_get_tile_type_count()` - Get number of available types 266 | - `map_tiles_get_tile_type_folder()` - Get folder name for a type 267 | 268 | ### Zoom Control 269 | - `map_tiles_set_zoom()` - Set zoom level 270 | - `map_tiles_get_zoom()` - Get current zoom level 271 | 272 | ### Error Handling 273 | - `map_tiles_set_loading_error()` - Set error state 274 | - `map_tiles_has_loading_error()` - Check error state 275 | 276 | ## Performance Considerations 277 | 278 | - **Memory Usage**: Each tile uses ~128KB (256×256×2 bytes) 279 | - **Grid Size**: Larger grids use more memory (3x3=9 tiles, 5x5=25 tiles, 7x7=49 tiles) 280 | - **SPIRAM**: Recommended for ESP32-S3 with PSRAM for better performance 281 | - **File System**: Ensure adequate file system performance for tile loading 282 | - **Tile Caching**: Component maintains tile buffers until cleanup 283 | 284 | ## Example Projects 285 | 286 | See the `examples` directory for complete implementation examples: 287 | - Basic map display 288 | - GPS tracking with map updates 289 | - Interactive map with touch controls 290 | 291 | ## License 292 | 293 | This component is released under the MIT License. See LICENSE file for details. 294 | 295 | ## Contributing 296 | 297 | Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests. 298 | 299 | ## Support 300 | 301 | For questions and support, please open an issue on the GitHub repository. 302 | -------------------------------------------------------------------------------- /map_tiles.cpp: -------------------------------------------------------------------------------- 1 | #include "map_tiles.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "esp_log.h" 7 | #include "esp_heap_caps.h" 8 | 9 | static const char* TAG = "map_tiles"; 10 | 11 | // Internal structure for map tiles instance 12 | struct map_tiles_t { 13 | // Configuration 14 | char* base_path; 15 | char* tile_folders[MAP_TILES_MAX_TYPES]; 16 | int tile_type_count; 17 | int current_tile_type; 18 | int grid_cols; 19 | int grid_rows; 20 | int tile_count; 21 | int zoom; 22 | bool use_spiram; 23 | bool initialized; 24 | 25 | // Tile management 26 | int tile_x; 27 | int tile_y; 28 | int marker_offset_x; 29 | int marker_offset_y; 30 | bool tile_loading_error; 31 | 32 | // Tile data - arrays will be allocated dynamically based on actual grid size 33 | uint8_t** tile_bufs; 34 | lv_image_dsc_t* tile_imgs; 35 | }; 36 | 37 | map_tiles_handle_t map_tiles_init(const map_tiles_config_t* config) 38 | { 39 | if (!config || !config->base_path || config->tile_type_count <= 0 || 40 | config->tile_type_count > MAP_TILES_MAX_TYPES || 41 | config->default_tile_type < 0 || config->default_tile_type >= config->tile_type_count) { 42 | ESP_LOGE(TAG, "Invalid configuration"); 43 | return NULL; 44 | } 45 | 46 | // Validate grid size - use defaults if not specified or invalid 47 | int grid_cols = config->grid_cols; 48 | int grid_rows = config->grid_rows; 49 | 50 | if (grid_cols <= 0 || grid_cols > MAP_TILES_MAX_GRID_COLS) { 51 | ESP_LOGW(TAG, "Invalid grid_cols %d, using default %d", grid_cols, MAP_TILES_DEFAULT_GRID_COLS); 52 | grid_cols = MAP_TILES_DEFAULT_GRID_COLS; 53 | } 54 | 55 | if (grid_rows <= 0 || grid_rows > MAP_TILES_MAX_GRID_ROWS) { 56 | ESP_LOGW(TAG, "Invalid grid_rows %d, using default %d", grid_rows, MAP_TILES_DEFAULT_GRID_ROWS); 57 | grid_rows = MAP_TILES_DEFAULT_GRID_ROWS; 58 | } 59 | 60 | int tile_count = grid_cols * grid_rows; 61 | 62 | // Validate that all tile folders are provided 63 | for (int i = 0; i < config->tile_type_count; i++) { 64 | if (!config->tile_folders[i]) { 65 | ESP_LOGE(TAG, "Tile folder %d is NULL", i); 66 | return NULL; 67 | } 68 | } 69 | 70 | map_tiles_handle_t handle = (map_tiles_handle_t)calloc(1, sizeof(struct map_tiles_t)); 71 | if (!handle) { 72 | ESP_LOGE(TAG, "Failed to allocate handle"); 73 | return NULL; 74 | } 75 | 76 | // Copy base path 77 | handle->base_path = strdup(config->base_path); 78 | if (!handle->base_path) { 79 | ESP_LOGE(TAG, "Failed to allocate base path"); 80 | free(handle); 81 | return NULL; 82 | } 83 | 84 | // Copy tile folder names 85 | handle->tile_type_count = config->tile_type_count; 86 | for (int i = 0; i < config->tile_type_count; i++) { 87 | handle->tile_folders[i] = strdup(config->tile_folders[i]); 88 | if (!handle->tile_folders[i]) { 89 | ESP_LOGE(TAG, "Failed to allocate tile folder %d", i); 90 | // Clean up previously allocated folders 91 | for (int j = 0; j < i; j++) { 92 | free(handle->tile_folders[j]); 93 | } 94 | free(handle->base_path); 95 | free(handle); 96 | return NULL; 97 | } 98 | } 99 | 100 | handle->zoom = config->default_zoom; 101 | handle->use_spiram = config->use_spiram; 102 | handle->current_tile_type = config->default_tile_type; 103 | handle->grid_cols = grid_cols; 104 | handle->grid_rows = grid_rows; 105 | handle->tile_count = tile_count; 106 | handle->initialized = true; 107 | handle->tile_loading_error = false; 108 | 109 | // Initialize tile data - allocate arrays based on actual tile count 110 | handle->tile_bufs = (uint8_t**)calloc(tile_count, sizeof(uint8_t*)); 111 | handle->tile_imgs = (lv_image_dsc_t*)calloc(tile_count, sizeof(lv_image_dsc_t)); 112 | 113 | if (!handle->tile_bufs || !handle->tile_imgs) { 114 | ESP_LOGE(TAG, "Failed to allocate tile arrays"); 115 | // Clean up 116 | if (handle->tile_bufs) free(handle->tile_bufs); 117 | if (handle->tile_imgs) free(handle->tile_imgs); 118 | for (int i = 0; i < handle->tile_type_count; i++) { 119 | free(handle->tile_folders[i]); 120 | } 121 | free(handle->base_path); 122 | free(handle); 123 | return NULL; 124 | } 125 | 126 | ESP_LOGI(TAG, "Map tiles initialized with base path: %s, %d tile types, current type: %s, zoom: %d, grid: %dx%d", 127 | handle->base_path, handle->tile_type_count, 128 | handle->tile_folders[handle->current_tile_type], handle->zoom, 129 | handle->grid_cols, handle->grid_rows); 130 | 131 | return handle; 132 | } 133 | 134 | void map_tiles_set_zoom(map_tiles_handle_t handle, int zoom_level) 135 | { 136 | if (!handle || !handle->initialized) { 137 | ESP_LOGE(TAG, "Handle not initialized"); 138 | return; 139 | } 140 | 141 | handle->zoom = zoom_level; 142 | ESP_LOGI(TAG, "Zoom level set to %d", zoom_level); 143 | } 144 | 145 | int map_tiles_get_zoom(map_tiles_handle_t handle) 146 | { 147 | if (!handle || !handle->initialized) { 148 | ESP_LOGE(TAG, "Handle not initialized"); 149 | return 0; 150 | } 151 | 152 | return handle->zoom; 153 | } 154 | 155 | bool map_tiles_set_tile_type(map_tiles_handle_t handle, int tile_type) 156 | { 157 | if (!handle || !handle->initialized) { 158 | ESP_LOGE(TAG, "Handle not initialized"); 159 | return false; 160 | } 161 | 162 | if (tile_type < 0 || tile_type >= handle->tile_type_count) { 163 | ESP_LOGE(TAG, "Invalid tile type: %d (valid range: 0-%d)", tile_type, handle->tile_type_count - 1); 164 | return false; 165 | } 166 | 167 | handle->current_tile_type = tile_type; 168 | ESP_LOGI(TAG, "Tile type set to %d (%s)", tile_type, handle->tile_folders[tile_type]); 169 | return true; 170 | } 171 | 172 | int map_tiles_get_tile_type(map_tiles_handle_t handle) 173 | { 174 | if (!handle || !handle->initialized) { 175 | ESP_LOGE(TAG, "Handle not initialized"); 176 | return -1; 177 | } 178 | 179 | return handle->current_tile_type; 180 | } 181 | 182 | int map_tiles_get_tile_type_count(map_tiles_handle_t handle) 183 | { 184 | if (!handle || !handle->initialized) { 185 | ESP_LOGE(TAG, "Handle not initialized"); 186 | return 0; 187 | } 188 | 189 | return handle->tile_type_count; 190 | } 191 | 192 | const char* map_tiles_get_tile_type_folder(map_tiles_handle_t handle, int tile_type) 193 | { 194 | if (!handle || !handle->initialized) { 195 | ESP_LOGE(TAG, "Handle not initialized"); 196 | return NULL; 197 | } 198 | 199 | if (tile_type < 0 || tile_type >= handle->tile_type_count) { 200 | ESP_LOGE(TAG, "Invalid tile type: %d", tile_type); 201 | return NULL; 202 | } 203 | 204 | return handle->tile_folders[tile_type]; 205 | } 206 | 207 | bool map_tiles_load_tile(map_tiles_handle_t handle, int index, int tile_x, int tile_y) 208 | { 209 | if (!handle || !handle->initialized) { 210 | ESP_LOGE(TAG, "Handle not initialized"); 211 | return false; 212 | } 213 | 214 | if (index < 0 || index >= handle->tile_count) { 215 | ESP_LOGE(TAG, "Invalid tile index: %d", index); 216 | return false; 217 | } 218 | 219 | char path[256]; 220 | const char* folder = handle->tile_folders[handle->current_tile_type]; 221 | snprintf(path, sizeof(path), "%s/%s/%d/%d/%d.bin", 222 | handle->base_path, folder, handle->zoom, tile_x, tile_y); 223 | 224 | FILE *f = fopen(path, "rb"); 225 | if (!f) { 226 | ESP_LOGW(TAG, "Tile not found: %s", path); 227 | return false; 228 | } 229 | 230 | // Skip 12-byte header 231 | fseek(f, 12, SEEK_SET); 232 | 233 | // Allocate buffer if needed 234 | if (!handle->tile_bufs[index]) { 235 | uint32_t caps = handle->use_spiram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : MALLOC_CAP_DMA; 236 | handle->tile_bufs[index] = (uint8_t*)heap_caps_malloc( 237 | MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL, caps); 238 | 239 | if (!handle->tile_bufs[index]) { 240 | ESP_LOGE(TAG, "Tile %d: allocation failed", index); 241 | fclose(f); 242 | return false; 243 | } 244 | } 245 | 246 | // Clear buffer 247 | memset(handle->tile_bufs[index], 0, 248 | MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL); 249 | 250 | // Read tile data 251 | size_t bytes_read = fread(handle->tile_bufs[index], 1, 252 | MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL, f); 253 | fclose(f); 254 | 255 | if (bytes_read != MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL) { 256 | ESP_LOGW(TAG, "Incomplete tile read: %zu bytes", bytes_read); 257 | } 258 | 259 | // Setup image descriptor 260 | handle->tile_imgs[index].header.w = MAP_TILES_TILE_SIZE; 261 | handle->tile_imgs[index].header.h = MAP_TILES_TILE_SIZE; 262 | handle->tile_imgs[index].header.cf = MAP_TILES_COLOR_FORMAT; 263 | handle->tile_imgs[index].header.stride = MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL; 264 | handle->tile_imgs[index].data = (const uint8_t*)handle->tile_bufs[index]; 265 | handle->tile_imgs[index].data_size = MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL; 266 | handle->tile_imgs[index].reserved = NULL; 267 | handle->tile_imgs[index].reserved_2 = NULL; 268 | 269 | ESP_LOGD(TAG, "Loaded tile %d from %s", index, path); 270 | return true; 271 | } 272 | 273 | void map_tiles_gps_to_tile_xy(map_tiles_handle_t handle, double lat, double lon, double* x, double* y) 274 | { 275 | if (!handle || !handle->initialized) { 276 | ESP_LOGE(TAG, "Handle not initialized"); 277 | return; 278 | } 279 | 280 | if (!x || !y) { 281 | ESP_LOGE(TAG, "Invalid output parameters"); 282 | return; 283 | } 284 | 285 | double lat_rad = lat * M_PI / 180.0; 286 | int n = 1 << handle->zoom; 287 | *x = (lon + 180.0) / 360.0 * n; 288 | *y = (1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * n; 289 | } 290 | 291 | void map_tiles_tile_xy_to_gps(map_tiles_handle_t handle, double x, double y, double* lat, double* lon) 292 | { 293 | if (!handle || !handle->initialized) { 294 | ESP_LOGE(TAG, "Handle not initialized"); 295 | return; 296 | } 297 | 298 | if (!lat || !lon) { 299 | ESP_LOGE(TAG, "Invalid output parameters"); 300 | return; 301 | } 302 | 303 | int n = 1 << handle->zoom; 304 | *lon = x / n * 360.0 - 180.0; 305 | double lat_rad = atan(sinh(M_PI * (1 - 2 * y / n))); 306 | *lat = lat_rad * 180.0 / M_PI; 307 | } 308 | 309 | void map_tiles_get_center_gps(map_tiles_handle_t handle, double* lat, double* lon) 310 | { 311 | if (!handle || !handle->initialized) { 312 | ESP_LOGE(TAG, "Handle not initialized"); 313 | return; 314 | } 315 | 316 | if (!lat || !lon) { 317 | ESP_LOGE(TAG, "Invalid output parameters"); 318 | return; 319 | } 320 | 321 | // Calculate center tile coordinates (center of the grid) 322 | double center_x = handle->tile_x + handle->grid_cols / 2.0; 323 | double center_y = handle->tile_y + handle->grid_rows / 2.0; 324 | 325 | // Convert to GPS coordinates 326 | map_tiles_tile_xy_to_gps(handle, center_x, center_y, lat, lon); 327 | } 328 | 329 | void map_tiles_set_center_from_gps(map_tiles_handle_t handle, double lat, double lon) 330 | { 331 | if (!handle || !handle->initialized) { 332 | ESP_LOGE(TAG, "Handle not initialized"); 333 | return; 334 | } 335 | 336 | double x, y; 337 | map_tiles_gps_to_tile_xy(handle, lat, lon, &x, &y); 338 | 339 | handle->tile_x = (int)x - handle->grid_cols / 2; 340 | handle->tile_y = (int)y - handle->grid_rows / 2; 341 | 342 | // Calculate pixel offset within the tile 343 | handle->marker_offset_x = (int)((x - (int)x) * MAP_TILES_TILE_SIZE); 344 | handle->marker_offset_y = (int)((y - (int)y) * MAP_TILES_TILE_SIZE); 345 | 346 | ESP_LOGI(TAG, "GPS to tile: tile_x=%d, tile_y=%d, offset_x=%d, offset_y=%d", 347 | handle->tile_x, handle->tile_y, handle->marker_offset_x, handle->marker_offset_y); 348 | } 349 | 350 | bool map_tiles_is_gps_within_tiles(map_tiles_handle_t handle, double lat, double lon) 351 | { 352 | if (!handle || !handle->initialized) { 353 | return false; 354 | } 355 | 356 | double x, y; 357 | map_tiles_gps_to_tile_xy(handle, lat, lon, &x, &y); 358 | 359 | int gps_tile_x = (int)x; 360 | int gps_tile_y = (int)y; 361 | 362 | bool within_x = (gps_tile_x >= handle->tile_x && gps_tile_x < handle->tile_x + handle->grid_cols); 363 | bool within_y = (gps_tile_y >= handle->tile_y && gps_tile_y < handle->tile_y + handle->grid_rows); 364 | 365 | return within_x && within_y; 366 | } 367 | 368 | void map_tiles_get_position(map_tiles_handle_t handle, int* tile_x, int* tile_y) 369 | { 370 | if (!handle || !handle->initialized) { 371 | ESP_LOGE(TAG, "Handle not initialized"); 372 | return; 373 | } 374 | 375 | if (tile_x) *tile_x = handle->tile_x; 376 | if (tile_y) *tile_y = handle->tile_y; 377 | } 378 | 379 | void map_tiles_set_position(map_tiles_handle_t handle, int tile_x, int tile_y) 380 | { 381 | if (!handle || !handle->initialized) { 382 | ESP_LOGE(TAG, "Handle not initialized"); 383 | return; 384 | } 385 | 386 | handle->tile_x = tile_x; 387 | handle->tile_y = tile_y; 388 | } 389 | 390 | void map_tiles_get_marker_offset(map_tiles_handle_t handle, int* offset_x, int* offset_y) 391 | { 392 | if (!handle || !handle->initialized) { 393 | ESP_LOGE(TAG, "Handle not initialized"); 394 | return; 395 | } 396 | 397 | if (offset_x) *offset_x = handle->marker_offset_x; 398 | if (offset_y) *offset_y = handle->marker_offset_y; 399 | } 400 | 401 | void map_tiles_set_marker_offset(map_tiles_handle_t handle, int offset_x, int offset_y) 402 | { 403 | if (!handle || !handle->initialized) { 404 | ESP_LOGE(TAG, "Handle not initialized"); 405 | return; 406 | } 407 | 408 | handle->marker_offset_x = offset_x; 409 | handle->marker_offset_y = offset_y; 410 | } 411 | 412 | lv_image_dsc_t* map_tiles_get_image(map_tiles_handle_t handle, int index) 413 | { 414 | if (!handle || !handle->initialized || index < 0 || index >= handle->tile_count) { 415 | return NULL; 416 | } 417 | 418 | return &handle->tile_imgs[index]; 419 | } 420 | 421 | uint8_t* map_tiles_get_buffer(map_tiles_handle_t handle, int index) 422 | { 423 | if (!handle || !handle->initialized || index < 0 || index >= handle->tile_count) { 424 | return NULL; 425 | } 426 | 427 | return handle->tile_bufs[index]; 428 | } 429 | 430 | void map_tiles_set_loading_error(map_tiles_handle_t handle, bool error) 431 | { 432 | if (!handle || !handle->initialized) { 433 | ESP_LOGE(TAG, "Handle not initialized"); 434 | return; 435 | } 436 | 437 | handle->tile_loading_error = error; 438 | } 439 | 440 | bool map_tiles_has_loading_error(map_tiles_handle_t handle) 441 | { 442 | if (!handle || !handle->initialized) { 443 | return true; 444 | } 445 | 446 | return handle->tile_loading_error; 447 | } 448 | 449 | void map_tiles_cleanup(map_tiles_handle_t handle) 450 | { 451 | if (!handle) { 452 | return; 453 | } 454 | 455 | if (handle->initialized) { 456 | // Free tile buffers 457 | if (handle->tile_bufs) { 458 | for (int i = 0; i < handle->tile_count; i++) { 459 | if (handle->tile_bufs[i]) { 460 | heap_caps_free(handle->tile_bufs[i]); 461 | handle->tile_bufs[i] = NULL; 462 | } 463 | } 464 | free(handle->tile_bufs); 465 | handle->tile_bufs = NULL; 466 | } 467 | 468 | // Free tile image descriptors array 469 | if (handle->tile_imgs) { 470 | free(handle->tile_imgs); 471 | handle->tile_imgs = NULL; 472 | } 473 | 474 | handle->initialized = false; 475 | ESP_LOGI(TAG, "Map tiles cleaned up"); 476 | } 477 | 478 | // Free base path and folder names, then handle 479 | if (handle->base_path) { 480 | free(handle->base_path); 481 | } 482 | for (int i = 0; i < handle->tile_type_count; i++) { 483 | if (handle->tile_folders[i]) { 484 | free(handle->tile_folders[i]); 485 | } 486 | } 487 | free(handle); 488 | } 489 | 490 | void map_tiles_get_grid_size(map_tiles_handle_t handle, int* cols, int* rows) 491 | { 492 | if (!handle || !handle->initialized || !cols || !rows) { 493 | if (cols) *cols = 0; 494 | if (rows) *rows = 0; 495 | return; 496 | } 497 | 498 | *cols = handle->grid_cols; 499 | *rows = handle->grid_rows; 500 | } 501 | 502 | int map_tiles_get_tile_count(map_tiles_handle_t handle) 503 | { 504 | if (!handle || !handle->initialized) { 505 | return 0; 506 | } 507 | 508 | return handle->tile_count; 509 | } 510 | --------------------------------------------------------------------------------