diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ab2554a Binary files /dev/null and b/.DS_Store differ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 0000000..a624130 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRCS "map_tiles.cpp" + INCLUDE_DIRS "include" + REQUIRES lvgl esp_system + PRIV_REQUIRES vfs fatfs +) diff --git a/README.md b/README.md new file mode 100755 index 0000000..a13154e --- /dev/null +++ b/README.md @@ -0,0 +1,279 @@ +# Map Tiles Component for LVGL 9.x + +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. + +## Features + +- **LVGL 9.x Compatible**: Fully compatible with LVGL 9.x image handling +- **GPS Coordinate Conversion**: Convert GPS coordinates to tile coordinates and vice versa +- **Dynamic Tile Loading**: Load map tiles on demand from file system +- **Configurable Grid Size**: Support for different grid sizes (3x3, 5x5, 7x7, etc.) +- **Multiple Tile Types**: Support for up to 8 different tile types (street, satellite, terrain, hybrid, etc.) +- **Memory Efficient**: Configurable memory allocation (SPIRAM or regular RAM) +- **Multiple Zoom Levels**: Support for different map zoom levels +- **Error Handling**: Comprehensive error handling and logging +- **C API**: Clean C API for easy integration + +## Requirements + +- ESP-IDF 5.0 or later +- LVGL 9.x +- File system support (FAT/SPIFFS/LittleFS) +- Map tiles in binary format (RGB565, 256x256 pixels) + +## Installation + +### Using ESP-IDF Component Manager + +Add to your project's `main/idf_component.yml`: + +```yaml +dependencies: + map_tiles: + git: "https://github.com/0015/map_tiles_component.git" + version: "^1.1.0" +``` + +### Manual Installation + +1. Copy the `map_tiles` folder to your project's `components` directory +2. The component will be automatically included in your build + +## Usage + +### Basic Setup + +```c +#include "map_tiles.h" + +// Configure the map tiles with multiple tile types and custom grid size +const char* tile_folders[] = {"street_map", "satellite", "terrain", "hybrid"}; +map_tiles_config_t config = { + .base_path = "/sdcard", // Base path to tile storage + .tile_folders = {tile_folders[0], tile_folders[1], tile_folders[2], tile_folders[3]}, + .tile_type_count = 4, // Number of tile types + .default_zoom = 10, // Default zoom level + .use_spiram = true, // Use SPIRAM if available + .default_tile_type = 0, // Start with street map (index 0) + .grid_cols = 5, // Grid width (tiles) + .grid_rows = 5 // Grid height (tiles) +}; + +// Initialize map tiles +map_tiles_handle_t map_handle = map_tiles_init(&config); +if (!map_handle) { + ESP_LOGE(TAG, "Failed to initialize map tiles"); + return; +} +``` + +### Loading Tiles + +```c +// Set center position from GPS coordinates +map_tiles_set_center_from_gps(map_handle, 37.7749, -122.4194); // San Francisco + +// Get grid dimensions +int grid_cols, grid_rows; +map_tiles_get_grid_size(map_handle, &grid_cols, &grid_rows); +int tile_count = map_tiles_get_tile_count(map_handle); + +// Load tiles for the configured grid size +for (int row = 0; row < grid_rows; row++) { + for (int col = 0; col < grid_cols; col++) { + int index = row * grid_cols + col; + int tile_x, tile_y; + map_tiles_get_position(map_handle, &tile_x, &tile_y); + + bool loaded = map_tiles_load_tile(map_handle, index, + tile_x + col, tile_y + row); + if (!loaded) { + ESP_LOGW(TAG, "Failed to load tile %d", index); + } + } +} +``` + +### Displaying Tiles with LVGL + +```c +// Get grid dimensions and tile count +int grid_cols, grid_rows; +map_tiles_get_grid_size(map_handle, &grid_cols, &grid_rows); +int tile_count = map_tiles_get_tile_count(map_handle); + +// Create image widgets for each tile +lv_obj_t** tile_images = malloc(tile_count * sizeof(lv_obj_t*)); + +for (int i = 0; i < tile_count; i++) { + tile_images[i] = lv_image_create(parent_container); + + // Get the tile image descriptor + lv_image_dsc_t* img_dsc = map_tiles_get_image(map_handle, i); + if (img_dsc) { + lv_image_set_src(tile_images[i], img_dsc); + + // Position the tile in the grid + int row = i / grid_cols; + int col = i % grid_cols; + lv_obj_set_pos(tile_images[i], + col * MAP_TILES_TILE_SIZE, + row * MAP_TILES_TILE_SIZE); + } +} +``` + +### Switching Tile Types + +```c +// Switch to different tile types +map_tiles_set_tile_type(map_handle, 0); // Street map +map_tiles_set_tile_type(map_handle, 1); // Satellite +map_tiles_set_tile_type(map_handle, 2); // Terrain +map_tiles_set_tile_type(map_handle, 3); // Hybrid + +// Get current tile type +int current_type = map_tiles_get_tile_type(map_handle); + +// Get available tile types +int type_count = map_tiles_get_tile_type_count(map_handle); +for (int i = 0; i < type_count; i++) { + const char* folder = map_tiles_get_tile_type_folder(map_handle, i); + printf("Tile type %d: %s\n", i, folder); +} +``` + +### GPS Coordinate Conversion + +```c +// Convert GPS to tile coordinates +double tile_x, tile_y; +map_tiles_gps_to_tile_xy(map_handle, 37.7749, -122.4194, &tile_x, &tile_y); + +// Check if GPS position is within current tile grid +bool within_tiles = map_tiles_is_gps_within_tiles(map_handle, 37.7749, -122.4194); + +// Get marker offset for precise positioning +int offset_x, offset_y; +map_tiles_get_marker_offset(map_handle, &offset_x, &offset_y); +``` + +### Memory Management + +```c +// Clean up when done +map_tiles_cleanup(map_handle); +``` + +## Tile File Format + +The component expects map tiles in a specific binary format: + +- **File Structure**: `{base_path}/{map_tile}/{zoom}/{tile_x}/{tile_y}.bin` +- **Format**: 12-byte header + raw RGB565 pixel data +- **Size**: 256x256 pixels +- **Color Format**: RGB565 (16-bit per pixel) + +### Example Tile Structure +``` +/sdcard/ +├── street_map/ // Tile type 0 +│ ├── 10/ +│ │ ├── 164/ +│ │ │ ├── 395.bin +│ │ │ ├── 396.bin +│ │ │ └── ... +│ │ └── ... +│ └── ... +├── satellite/ // Tile type 1 +│ ├── 10/ +│ │ ├── 164/ +│ │ │ ├── 395.bin +│ │ │ └── ... +│ │ └── ... +│ └── ... +├── terrain/ // Tile type 2 +│ └── ... +└── hybrid/ // Tile type 3 + └── ... +``` + +## Configuration Options + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| `base_path` | `const char*` | Base directory for tile storage | Required | +| `tile_folders` | `const char*[]` | Array of folder names for different tile types | Required | +| `tile_type_count` | `int` | Number of tile types (max 8) | Required | +| `default_zoom` | `int` | Initial zoom level | Required | +| `use_spiram` | `bool` | Use SPIRAM for tile buffers | `false` | +| `default_tile_type` | `int` | Initial tile type index | Required | +| `grid_cols` | `int` | Number of tile columns (max 10) | 5 | +| `grid_rows` | `int` | Number of tile rows (max 10) | 5 | + +## API Reference + +### Initialization +- `map_tiles_init()` - Initialize map tiles system +- `map_tiles_cleanup()` - Clean up resources + +### Tile Management +- `map_tiles_load_tile()` - Load a specific tile +- `map_tiles_get_image()` - Get LVGL image descriptor +- `map_tiles_get_buffer()` - Get raw tile buffer + +### Grid Management +- `map_tiles_get_grid_size()` - Get current grid dimensions +- `map_tiles_get_tile_count()` - Get total number of tiles in grid + +### Coordinate Conversion +- `map_tiles_gps_to_tile_xy()` - Convert GPS to tile coordinates +- `map_tiles_set_center_from_gps()` - Set center from GPS +- `map_tiles_is_gps_within_tiles()` - Check if GPS is within current tiles + +### Position Management +- `map_tiles_get_position()` - Get current tile position +- `map_tiles_set_position()` - Set tile position +- `map_tiles_get_marker_offset()` - Get marker offset +- `map_tiles_set_marker_offset()` - Set marker offset + +### Tile Type Management +- `map_tiles_set_tile_type()` - Set active tile type +- `map_tiles_get_tile_type()` - Get current tile type +- `map_tiles_get_tile_type_count()` - Get number of available types +- `map_tiles_get_tile_type_folder()` - Get folder name for a type + +### Zoom Control +- `map_tiles_set_zoom()` - Set zoom level +- `map_tiles_get_zoom()` - Get current zoom level + +### Error Handling +- `map_tiles_set_loading_error()` - Set error state +- `map_tiles_has_loading_error()` - Check error state + +## Performance Considerations + +- **Memory Usage**: Each tile uses ~128KB (256×256×2 bytes) +- **Grid Size**: Larger grids use more memory (3x3=9 tiles, 5x5=25 tiles, 7x7=49 tiles) +- **SPIRAM**: Recommended for ESP32-S3 with PSRAM for better performance +- **File System**: Ensure adequate file system performance for tile loading +- **Tile Caching**: Component maintains tile buffers until cleanup + +## Example Projects + +See the `examples` directory for complete implementation examples: +- Basic map display +- GPS tracking with map updates +- Interactive map with touch controls + +## License + +This component is released under the MIT License. See LICENSE file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests. + +## Support + +For questions and support, please open an issue on the GitHub repository. diff --git a/examples/basic_map_display.c b/examples/basic_map_display.c new file mode 100755 index 0000000..14287e1 --- /dev/null +++ b/examples/basic_map_display.c @@ -0,0 +1,286 @@ +#include "map_tiles.h" +#include "lvgl.h" +#include "esp_log.h" + +static const char* TAG = "map_example"; + +// Map tiles handle +static map_tiles_handle_t map_handle = NULL; + +// LVGL objects for displaying tiles +static lv_obj_t* map_container = NULL; +static lv_obj_t** tile_images = NULL; // Dynamic array for configurable grid +static int grid_cols = 0, grid_rows = 0, tile_count = 0; + +/** + * @brief Initialize the map display + */ +void map_display_init(void) +{ + // Configure map tiles with multiple tile types and custom grid size + const char* tile_folders[] = {"street_map", "satellite", "terrain", "hybrid"}; + map_tiles_config_t config = { + .base_path = "/sdcard", + .tile_folders = {tile_folders[0], tile_folders[1], tile_folders[2], tile_folders[3]}, + .tile_type_count = 4, + .default_zoom = 10, + .use_spiram = true, + .default_tile_type = 0, // Start with street map + .grid_cols = 5, // 5x5 grid (configurable) + .grid_rows = 5 + }; + + // Initialize map tiles + map_handle = map_tiles_init(&config); + if (!map_handle) { + ESP_LOGE(TAG, "Failed to initialize map tiles"); + return; + } + + // Get grid dimensions + map_tiles_get_grid_size(map_handle, &grid_cols, &grid_rows); + tile_count = map_tiles_get_tile_count(map_handle); + + // Allocate tile images array + tile_images = malloc(tile_count * sizeof(lv_obj_t*)); + if (!tile_images) { + ESP_LOGE(TAG, "Failed to allocate tile images array"); + map_tiles_cleanup(map_handle); + return; + } + + // Create map container + map_container = lv_obj_create(lv_screen_active()); + lv_obj_set_size(map_container, + grid_cols * MAP_TILES_TILE_SIZE, + grid_rows * MAP_TILES_TILE_SIZE); + lv_obj_center(map_container); + lv_obj_set_style_pad_all(map_container, 0, 0); + lv_obj_set_style_border_width(map_container, 0, 0); + + // Create image widgets for each tile + for (int i = 0; i < tile_count; i++) { + tile_images[i] = lv_image_create(map_container); + + // Position tile in grid + int row = i / grid_cols; + int col = i % grid_cols; + lv_obj_set_pos(tile_images[i], + col * MAP_TILES_TILE_SIZE, + row * MAP_TILES_TILE_SIZE); + lv_obj_set_size(tile_images[i], MAP_TILES_TILE_SIZE, MAP_TILES_TILE_SIZE); + } + + ESP_LOGI(TAG, "Map display initialized"); +} + +/** + * @brief Load and display map tiles for a GPS location + * + * @param lat Latitude in degrees + * @param lon Longitude in degrees + */ +void map_display_load_location(double lat, double lon) +{ + if (!map_handle) { + ESP_LOGE(TAG, "Map not initialized"); + return; + } + + ESP_LOGI(TAG, "Loading map for GPS: %.6f, %.6f", lat, lon); + + // Set center from GPS coordinates + map_tiles_set_center_from_gps(map_handle, lat, lon); + + // Get current tile position + int base_tile_x, base_tile_y; + map_tiles_get_position(map_handle, &base_tile_x, &base_tile_y); + + // Load tiles in a configurable grid + for (int row = 0; row < grid_rows; row++) { + for (int col = 0; col < grid_cols; col++) { + int index = row * grid_cols + col; + int tile_x = base_tile_x + col; + int tile_y = base_tile_y + row; + + // Load the tile + bool loaded = map_tiles_load_tile(map_handle, index, tile_x, tile_y); + if (loaded) { + // Update the image widget + lv_image_dsc_t* img_dsc = map_tiles_get_image(map_handle, index); + if (img_dsc) { + lv_image_set_src(tile_images[index], img_dsc); + ESP_LOGD(TAG, "Loaded tile %d (%d, %d)", index, tile_x, tile_y); + } + } else { + ESP_LOGW(TAG, "Failed to load tile %d (%d, %d)", index, tile_x, tile_y); + // Set a placeholder or clear the image + lv_image_set_src(tile_images[index], NULL); + } + } + } + + ESP_LOGI(TAG, "Map tiles loaded for location"); +} + +/** + * @brief Set the map tile type and reload tiles + * + * @param tile_type Tile type index (0=street, 1=satellite, 2=terrain, 3=hybrid) + * @param lat Current latitude + * @param lon Current longitude + */ +void map_display_set_tile_type(int tile_type, double lat, double lon) +{ + if (!map_handle) { + ESP_LOGE(TAG, "Map not initialized"); + return; + } + + // Validate tile type + int max_types = map_tiles_get_tile_type_count(map_handle); + if (tile_type < 0 || tile_type >= max_types) { + ESP_LOGW(TAG, "Invalid tile type %d (valid range: 0-%d)", tile_type, max_types - 1); + return; + } + + ESP_LOGI(TAG, "Setting tile type to %d (%s)", tile_type, + map_tiles_get_tile_type_folder(map_handle, tile_type)); + + // Set tile type + if (map_tiles_set_tile_type(map_handle, tile_type)) { + // Reload tiles for the new type + map_display_load_location(lat, lon); + } +} + +/** + * @brief Set the zoom level and reload tiles + * + * @param zoom New zoom level + * @param lat Current latitude + * @param lon Current longitude + */ +void map_display_set_zoom(int zoom, double lat, double lon) +{ + if (!map_handle) { + ESP_LOGE(TAG, "Map not initialized"); + return; + } + + ESP_LOGI(TAG, "Setting zoom to %d", zoom); + + // Update zoom level + map_tiles_set_zoom(map_handle, zoom); + + // Reload tiles for the new zoom level + map_display_load_location(lat, lon); +} + +/** + * @brief Add a GPS marker to the map + * + * @param lat Latitude in degrees + * @param lon Longitude in degrees + */ +void map_display_add_marker(double lat, double lon) +{ + if (!map_handle) { + ESP_LOGE(TAG, "Map not initialized"); + return; + } + + // Check if GPS position is within current tiles + if (!map_tiles_is_gps_within_tiles(map_handle, lat, lon)) { + ESP_LOGW(TAG, "GPS position outside current tiles, reloading map"); + map_display_load_location(lat, lon); + return; + } + + // Get marker offset within the current tile grid + int offset_x, offset_y; + map_tiles_get_marker_offset(map_handle, &offset_x, &offset_y); + + // Create or update marker object + static lv_obj_t* marker = NULL; + if (!marker) { + marker = lv_obj_create(map_container); + lv_obj_set_size(marker, 10, 10); + lv_obj_set_style_bg_color(marker, lv_color_hex(0xFF0000), 0); + lv_obj_set_style_radius(marker, 5, 0); + lv_obj_set_style_border_width(marker, 1, 0); + lv_obj_set_style_border_color(marker, lv_color_hex(0xFFFFFF), 0); + } + + // Position marker based on GPS offset + int center_tile_col = grid_cols / 2; + int center_tile_row = grid_rows / 2; + int marker_x = center_tile_col * MAP_TILES_TILE_SIZE + offset_x - 5; + int marker_y = center_tile_row * MAP_TILES_TILE_SIZE + offset_y - 5; + + lv_obj_set_pos(marker, marker_x, marker_y); + + ESP_LOGI(TAG, "GPS marker positioned at (%d, %d)", marker_x, marker_y); +} + +/** + * @brief Clean up map display resources + */ +void map_display_cleanup(void) +{ + if (tile_images) { + free(tile_images); + tile_images = NULL; + } + + if (map_handle) { + map_tiles_cleanup(map_handle); + map_handle = NULL; + } + + if (map_container) { + lv_obj_delete(map_container); + map_container = NULL; + } + + grid_cols = grid_rows = tile_count = 0; + + ESP_LOGI(TAG, "Map display cleaned up"); +} + +/** + * @brief Example usage in main application + */ +void app_main(void) +{ + // Initialize LVGL and display driver first... + + // Initialize map display + map_display_init(); + + // Load map for San Francisco + double lat = 37.7749; + double lon = -122.4194; + map_display_load_location(lat, lon); + + // Add GPS marker + map_display_add_marker(lat, lon); + + // Example: Change to satellite view (tile type 1) + // map_display_set_tile_type(1, lat, lon); + + // Example: Change to terrain view (tile type 2) + // map_display_set_tile_type(2, lat, lon); + + // Example: Change zoom level + // map_display_set_zoom(12, lat, lon); + + // Example: Update GPS position + // map_display_add_marker(37.7849, -122.4094); + + // NOTE: To use different grid sizes, modify the grid_cols and grid_rows + // in the config structure above. Examples: + // - 3x3 grid: .grid_cols = 3, .grid_rows = 3 (9 tiles, ~1.1MB RAM) + // - 5x5 grid: .grid_cols = 5, .grid_rows = 5 (25 tiles, ~3.1MB RAM) + // - 7x7 grid: .grid_cols = 7, .grid_rows = 7 (49 tiles, ~6.1MB RAM) +} diff --git a/idf_component.yml b/idf_component.yml new file mode 100755 index 0000000..db6e6f1 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,27 @@ +version: "1.1.0" +maintainers: + - Eric Nam +description: "Map tiles component for LVGL 9.x - Load and display map tiles with GPS coordinate conversion" +url: "https://github.com/0015/map_tiles" +license: "MIT" + +dependencies: + lvgl/lvgl: ">=9.3.0" + idf: ">=5.0" + +targets: + - esp32 + - esp32s2 + - esp32s3 + - esp32c3 + - esp32c6 + - esp32h2 + - esp32p4 + +keywords: + - lvgl + - map + - tiles + - gps + - graphics + - ui diff --git a/include/map_tiles.h b/include/map_tiles.h new file mode 100755 index 0000000..521c6c1 --- /dev/null +++ b/include/map_tiles.h @@ -0,0 +1,264 @@ +#pragma once + +#include +#include +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Map tiles component for LVGL 9.x + * + * This component provides functionality to load and display map tiles with GPS coordinate conversion. + * Tiles are assumed to be 256x256 pixels in RGB565 format, stored in binary files. + */ + +// Constants +#define MAP_TILES_TILE_SIZE 256 +#define MAP_TILES_DEFAULT_GRID_COLS 5 +#define MAP_TILES_DEFAULT_GRID_ROWS 5 +#define MAP_TILES_MAX_GRID_COLS 9 +#define MAP_TILES_MAX_GRID_ROWS 9 +#define MAP_TILES_MAX_TILES (MAP_TILES_MAX_GRID_COLS * MAP_TILES_MAX_GRID_ROWS) +#define MAP_TILES_BYTES_PER_PIXEL 2 +#define MAP_TILES_COLOR_FORMAT LV_COLOR_FORMAT_RGB565 +#define MAP_TILES_MAX_TYPES 8 +#define MAP_TILES_MAX_FOLDER_NAME 32 + +/** + * @brief Configuration structure for map tiles + */ +typedef struct { + const char* base_path; /**< Base path where tiles are stored (e.g., "/sdcard") */ + const char* tile_folders[MAP_TILES_MAX_TYPES]; /**< Array of folder names for different tile types */ + int tile_type_count; /**< Number of tile types configured */ + int grid_cols; /**< Number of tile columns (default: 5, max: 9) */ + int grid_rows; /**< Number of tile rows (default: 5, max: 9) */ + int default_zoom; /**< Default zoom level */ + bool use_spiram; /**< Whether to use SPIRAM for tile buffers */ + int default_tile_type; /**< Default tile type index (0 to tile_type_count-1) */ +} map_tiles_config_t; + +/** + * @brief Map tiles handle + */ +typedef struct map_tiles_t* map_tiles_handle_t; + +/** + * @brief Initialize the map tiles system + * + * @param config Configuration structure + * @return map_tiles_handle_t Handle to the map tiles instance, NULL on failure + */ +map_tiles_handle_t map_tiles_init(const map_tiles_config_t* config); + +/** + * @brief Set the zoom level + * + * @param handle Map tiles handle + * @param zoom_level Zoom level (typically 0-18) + */ +void map_tiles_set_zoom(map_tiles_handle_t handle, int zoom_level); + +/** + * @brief Get the current zoom level + * + * @param handle Map tiles handle + * @return Current zoom level + */ +int map_tiles_get_zoom(map_tiles_handle_t handle); + +/** + * @brief Set tile type + * + * @param handle Map tiles handle + * @param tile_type Tile type index (0 to configured tile_type_count-1) + * @return true if tile type was set successfully, false otherwise + */ +bool map_tiles_set_tile_type(map_tiles_handle_t handle, int tile_type); + +/** + * @brief Get current tile type + * + * @param handle Map tiles handle + * @return Current tile type index, -1 if error + */ +int map_tiles_get_tile_type(map_tiles_handle_t handle); + +/** + * @brief Get grid dimensions + * + * @param handle Map tiles handle + * @param cols Output grid columns (can be NULL) + * @param rows Output grid rows (can be NULL) + */ +void map_tiles_get_grid_size(map_tiles_handle_t handle, int* cols, int* rows); + +/** + * @brief Get total tile count for current grid + * + * @param handle Map tiles handle + * @return Total number of tiles in the grid, 0 if error + */ +int map_tiles_get_tile_count(map_tiles_handle_t handle); + +/** + * @brief Get tile type count + * + * @param handle Map tiles handle + * @return Number of configured tile types, 0 if error + */ +int map_tiles_get_tile_type_count(map_tiles_handle_t handle); + +/** + * @brief Get tile type folder name + * + * @param handle Map tiles handle + * @param tile_type Tile type index + * @return Folder name for the tile type, NULL if invalid + */ +const char* map_tiles_get_tile_type_folder(map_tiles_handle_t handle, int tile_type); + +/** + * @brief Load a specific tile dynamically + * + * @param handle Map tiles handle + * @param index Tile index (0 to total_tile_count-1) + * @param tile_x Tile X coordinate + * @param tile_y Tile Y coordinate + * @return true if tile loaded successfully, false otherwise + */ +bool map_tiles_load_tile(map_tiles_handle_t handle, int index, int tile_x, int tile_y); + +/** + * @brief Convert GPS coordinates to tile coordinates + * + * @param handle Map tiles handle + * @param lat Latitude in degrees + * @param lon Longitude in degrees + * @param x Output tile X coordinate + * @param y Output tile Y coordinate + */ +void map_tiles_gps_to_tile_xy(map_tiles_handle_t handle, double lat, double lon, double* x, double* y); + +/** + * @brief Convert tile coordinates to GPS coordinates + * + * @param handle Map tiles handle + * @param x Tile X coordinate + * @param y Tile Y coordinate + * @param lat Output latitude in degrees + * @param lon Output longitude in degrees + */ +void map_tiles_tile_xy_to_gps(map_tiles_handle_t handle, double x, double y, double* lat, double* lon); + +/** + * @brief Get center GPS coordinates of current map view + * + * @param handle Map tiles handle + * @param lat Output latitude in degrees + * @param lon Output longitude in degrees + */ +void map_tiles_get_center_gps(map_tiles_handle_t handle, double* lat, double* lon); + +/** + * @brief Set the tile center from GPS coordinates + * + * @param handle Map tiles handle + * @param lat Latitude in degrees + * @param lon Longitude in degrees + */ +void map_tiles_set_center_from_gps(map_tiles_handle_t handle, double lat, double lon); + +/** + * @brief Check if GPS coordinates are within current tile grid + * + * @param handle Map tiles handle + * @param lat Latitude in degrees + * @param lon Longitude in degrees + * @return true if GPS position is within current tiles, false otherwise + */ +bool map_tiles_is_gps_within_tiles(map_tiles_handle_t handle, double lat, double lon); + +/** + * @brief Get current tile position + * + * @param handle Map tiles handle + * @param tile_x Output tile X coordinate (can be NULL) + * @param tile_y Output tile Y coordinate (can be NULL) + */ +void map_tiles_get_position(map_tiles_handle_t handle, int* tile_x, int* tile_y); + +/** + * @brief Set tile position + * + * @param handle Map tiles handle + * @param tile_x Tile X coordinate + * @param tile_y Tile Y coordinate + */ +void map_tiles_set_position(map_tiles_handle_t handle, int tile_x, int tile_y); + +/** + * @brief Get marker offset within the current tile + * + * @param handle Map tiles handle + * @param offset_x Output X offset in pixels (can be NULL) + * @param offset_y Output Y offset in pixels (can be NULL) + */ +void map_tiles_get_marker_offset(map_tiles_handle_t handle, int* offset_x, int* offset_y); + +/** + * @brief Set marker offset within the current tile + * + * @param handle Map tiles handle + * @param offset_x X offset in pixels + * @param offset_y Y offset in pixels + */ +void map_tiles_set_marker_offset(map_tiles_handle_t handle, int offset_x, int offset_y); + +/** + * @brief Get tile image descriptor + * + * @param handle Map tiles handle + * @param index Tile index (0 to total_tile_count-1) + * @return Pointer to LVGL image descriptor, NULL if invalid + */ +lv_image_dsc_t* map_tiles_get_image(map_tiles_handle_t handle, int index); + +/** + * @brief Get tile buffer + * + * @param handle Map tiles handle + * @param index Tile index (0 to total_tile_count-1) + * @return Pointer to tile buffer, NULL if invalid + */ +uint8_t* map_tiles_get_buffer(map_tiles_handle_t handle, int index); + +/** + * @brief Set tile loading error state + * + * @param handle Map tiles handle + * @param error Error state + */ +void map_tiles_set_loading_error(map_tiles_handle_t handle, bool error); + +/** + * @brief Check if there's a tile loading error + * + * @param handle Map tiles handle + * @return true if there's an error, false otherwise + */ +bool map_tiles_has_loading_error(map_tiles_handle_t handle); + +/** + * @brief Clean up and free map tiles resources + * + * @param handle Map tiles handle + */ +void map_tiles_cleanup(map_tiles_handle_t handle); + +#ifdef __cplusplus +} +#endif diff --git a/map_tiles.cpp b/map_tiles.cpp new file mode 100755 index 0000000..74ce306 --- /dev/null +++ b/map_tiles.cpp @@ -0,0 +1,509 @@ +#include "map_tiles.h" +#include +#include +#include +#include +#include "esp_log.h" +#include "esp_heap_caps.h" + +static const char* TAG = "map_tiles"; + +// Internal structure for map tiles instance +struct map_tiles_t { + // Configuration + char* base_path; + char* tile_folders[MAP_TILES_MAX_TYPES]; + int tile_type_count; + int current_tile_type; + int grid_cols; + int grid_rows; + int tile_count; + int zoom; + bool use_spiram; + bool initialized; + + // Tile management + int tile_x; + int tile_y; + int marker_offset_x; + int marker_offset_y; + bool tile_loading_error; + + // Tile data - arrays will be allocated dynamically based on actual grid size + uint8_t** tile_bufs; + lv_image_dsc_t* tile_imgs; +}; + +map_tiles_handle_t map_tiles_init(const map_tiles_config_t* config) +{ + if (!config || !config->base_path || config->tile_type_count <= 0 || + config->tile_type_count > MAP_TILES_MAX_TYPES || + config->default_tile_type < 0 || config->default_tile_type >= config->tile_type_count) { + ESP_LOGE(TAG, "Invalid configuration"); + return NULL; + } + + // Validate grid size - use defaults if not specified or invalid + int grid_cols = config->grid_cols; + int grid_rows = config->grid_rows; + + if (grid_cols <= 0 || grid_cols > MAP_TILES_MAX_GRID_COLS) { + ESP_LOGW(TAG, "Invalid grid_cols %d, using default %d", grid_cols, MAP_TILES_DEFAULT_GRID_COLS); + grid_cols = MAP_TILES_DEFAULT_GRID_COLS; + } + + if (grid_rows <= 0 || grid_rows > MAP_TILES_MAX_GRID_ROWS) { + ESP_LOGW(TAG, "Invalid grid_rows %d, using default %d", grid_rows, MAP_TILES_DEFAULT_GRID_ROWS); + grid_rows = MAP_TILES_DEFAULT_GRID_ROWS; + } + + int tile_count = grid_cols * grid_rows; + + // Validate that all tile folders are provided + for (int i = 0; i < config->tile_type_count; i++) { + if (!config->tile_folders[i]) { + ESP_LOGE(TAG, "Tile folder %d is NULL", i); + return NULL; + } + } + + map_tiles_handle_t handle = (map_tiles_handle_t)calloc(1, sizeof(struct map_tiles_t)); + if (!handle) { + ESP_LOGE(TAG, "Failed to allocate handle"); + return NULL; + } + + // Copy base path + handle->base_path = strdup(config->base_path); + if (!handle->base_path) { + ESP_LOGE(TAG, "Failed to allocate base path"); + free(handle); + return NULL; + } + + // Copy tile folder names + handle->tile_type_count = config->tile_type_count; + for (int i = 0; i < config->tile_type_count; i++) { + handle->tile_folders[i] = strdup(config->tile_folders[i]); + if (!handle->tile_folders[i]) { + ESP_LOGE(TAG, "Failed to allocate tile folder %d", i); + // Clean up previously allocated folders + for (int j = 0; j < i; j++) { + free(handle->tile_folders[j]); + } + free(handle->base_path); + free(handle); + return NULL; + } + } + + handle->zoom = config->default_zoom; + handle->use_spiram = config->use_spiram; + handle->current_tile_type = config->default_tile_type; + handle->grid_cols = grid_cols; + handle->grid_rows = grid_rows; + handle->tile_count = tile_count; + handle->initialized = true; + handle->tile_loading_error = false; + + // Initialize tile data - allocate arrays based on actual tile count + handle->tile_bufs = (uint8_t**)calloc(tile_count, sizeof(uint8_t*)); + handle->tile_imgs = (lv_image_dsc_t*)calloc(tile_count, sizeof(lv_image_dsc_t)); + + if (!handle->tile_bufs || !handle->tile_imgs) { + ESP_LOGE(TAG, "Failed to allocate tile arrays"); + // Clean up + if (handle->tile_bufs) free(handle->tile_bufs); + if (handle->tile_imgs) free(handle->tile_imgs); + for (int i = 0; i < handle->tile_type_count; i++) { + free(handle->tile_folders[i]); + } + free(handle->base_path); + free(handle); + return NULL; + } + + ESP_LOGI(TAG, "Map tiles initialized with base path: %s, %d tile types, current type: %s, zoom: %d, grid: %dx%d", + handle->base_path, handle->tile_type_count, + handle->tile_folders[handle->current_tile_type], handle->zoom, + handle->grid_cols, handle->grid_rows); + + return handle; +} + +void map_tiles_set_zoom(map_tiles_handle_t handle, int zoom_level) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + handle->zoom = zoom_level; + ESP_LOGI(TAG, "Zoom level set to %d", zoom_level); +} + +int map_tiles_get_zoom(map_tiles_handle_t handle) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return 0; + } + + return handle->zoom; +} + +bool map_tiles_set_tile_type(map_tiles_handle_t handle, int tile_type) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return false; + } + + if (tile_type < 0 || tile_type >= handle->tile_type_count) { + ESP_LOGE(TAG, "Invalid tile type: %d (valid range: 0-%d)", tile_type, handle->tile_type_count - 1); + return false; + } + + handle->current_tile_type = tile_type; + ESP_LOGI(TAG, "Tile type set to %d (%s)", tile_type, handle->tile_folders[tile_type]); + return true; +} + +int map_tiles_get_tile_type(map_tiles_handle_t handle) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return -1; + } + + return handle->current_tile_type; +} + +int map_tiles_get_tile_type_count(map_tiles_handle_t handle) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return 0; + } + + return handle->tile_type_count; +} + +const char* map_tiles_get_tile_type_folder(map_tiles_handle_t handle, int tile_type) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return NULL; + } + + if (tile_type < 0 || tile_type >= handle->tile_type_count) { + ESP_LOGE(TAG, "Invalid tile type: %d", tile_type); + return NULL; + } + + return handle->tile_folders[tile_type]; +} + +bool map_tiles_load_tile(map_tiles_handle_t handle, int index, int tile_x, int tile_y) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return false; + } + + if (index < 0 || index >= handle->tile_count) { + ESP_LOGE(TAG, "Invalid tile index: %d", index); + return false; + } + + char path[256]; + const char* folder = handle->tile_folders[handle->current_tile_type]; + snprintf(path, sizeof(path), "%s/%s/%d/%d/%d.bin", + handle->base_path, folder, handle->zoom, tile_x, tile_y); + + FILE *f = fopen(path, "rb"); + if (!f) { + ESP_LOGW(TAG, "Tile not found: %s", path); + return false; + } + + // Skip 12-byte header + fseek(f, 12, SEEK_SET); + + // Allocate buffer if needed + if (!handle->tile_bufs[index]) { + uint32_t caps = handle->use_spiram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : MALLOC_CAP_DMA; + handle->tile_bufs[index] = (uint8_t*)heap_caps_malloc( + MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL, caps); + + if (!handle->tile_bufs[index]) { + ESP_LOGE(TAG, "Tile %d: allocation failed", index); + fclose(f); + return false; + } + } + + // Clear buffer + memset(handle->tile_bufs[index], 0, + MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL); + + // Read tile data + size_t bytes_read = fread(handle->tile_bufs[index], 1, + MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL, f); + fclose(f); + + if (bytes_read != MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL) { + ESP_LOGW(TAG, "Incomplete tile read: %zu bytes", bytes_read); + } + + // Setup image descriptor + handle->tile_imgs[index].header.w = MAP_TILES_TILE_SIZE; + handle->tile_imgs[index].header.h = MAP_TILES_TILE_SIZE; + handle->tile_imgs[index].header.cf = MAP_TILES_COLOR_FORMAT; + handle->tile_imgs[index].header.stride = MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL; + handle->tile_imgs[index].data = (const uint8_t*)handle->tile_bufs[index]; + handle->tile_imgs[index].data_size = MAP_TILES_TILE_SIZE * MAP_TILES_TILE_SIZE * MAP_TILES_BYTES_PER_PIXEL; + handle->tile_imgs[index].reserved = NULL; + handle->tile_imgs[index].reserved_2 = NULL; + + ESP_LOGD(TAG, "Loaded tile %d from %s", index, path); + return true; +} + +void map_tiles_gps_to_tile_xy(map_tiles_handle_t handle, double lat, double lon, double* x, double* y) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + if (!x || !y) { + ESP_LOGE(TAG, "Invalid output parameters"); + return; + } + + double lat_rad = lat * M_PI / 180.0; + int n = 1 << handle->zoom; + *x = (lon + 180.0) / 360.0 * n; + *y = (1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * n; +} + +void map_tiles_tile_xy_to_gps(map_tiles_handle_t handle, double x, double y, double* lat, double* lon) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + if (!lat || !lon) { + ESP_LOGE(TAG, "Invalid output parameters"); + return; + } + + int n = 1 << handle->zoom; + *lon = x / n * 360.0 - 180.0; + double lat_rad = atan(sinh(M_PI * (1 - 2 * y / n))); + *lat = lat_rad * 180.0 / M_PI; +} + +void map_tiles_get_center_gps(map_tiles_handle_t handle, double* lat, double* lon) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + if (!lat || !lon) { + ESP_LOGE(TAG, "Invalid output parameters"); + return; + } + + // Calculate center tile coordinates (center of the grid) + double center_x = handle->tile_x + handle->grid_cols / 2.0; + double center_y = handle->tile_y + handle->grid_rows / 2.0; + + // Convert to GPS coordinates + map_tiles_tile_xy_to_gps(handle, center_x, center_y, lat, lon); +} + +void map_tiles_set_center_from_gps(map_tiles_handle_t handle, double lat, double lon) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + double x, y; + map_tiles_gps_to_tile_xy(handle, lat, lon, &x, &y); + + handle->tile_x = (int)x - handle->grid_cols / 2; + handle->tile_y = (int)y - handle->grid_rows / 2; + + // Calculate pixel offset within the tile + handle->marker_offset_x = (int)((x - (int)x) * MAP_TILES_TILE_SIZE); + handle->marker_offset_y = (int)((y - (int)y) * MAP_TILES_TILE_SIZE); + + ESP_LOGI(TAG, "GPS to tile: tile_x=%d, tile_y=%d, offset_x=%d, offset_y=%d", + handle->tile_x, handle->tile_y, handle->marker_offset_x, handle->marker_offset_y); +} + +bool map_tiles_is_gps_within_tiles(map_tiles_handle_t handle, double lat, double lon) +{ + if (!handle || !handle->initialized) { + return false; + } + + double x, y; + map_tiles_gps_to_tile_xy(handle, lat, lon, &x, &y); + + int gps_tile_x = (int)x; + int gps_tile_y = (int)y; + + bool within_x = (gps_tile_x >= handle->tile_x && gps_tile_x < handle->tile_x + handle->grid_cols); + bool within_y = (gps_tile_y >= handle->tile_y && gps_tile_y < handle->tile_y + handle->grid_rows); + + return within_x && within_y; +} + +void map_tiles_get_position(map_tiles_handle_t handle, int* tile_x, int* tile_y) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + if (tile_x) *tile_x = handle->tile_x; + if (tile_y) *tile_y = handle->tile_y; +} + +void map_tiles_set_position(map_tiles_handle_t handle, int tile_x, int tile_y) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + handle->tile_x = tile_x; + handle->tile_y = tile_y; +} + +void map_tiles_get_marker_offset(map_tiles_handle_t handle, int* offset_x, int* offset_y) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + if (offset_x) *offset_x = handle->marker_offset_x; + if (offset_y) *offset_y = handle->marker_offset_y; +} + +void map_tiles_set_marker_offset(map_tiles_handle_t handle, int offset_x, int offset_y) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + handle->marker_offset_x = offset_x; + handle->marker_offset_y = offset_y; +} + +lv_image_dsc_t* map_tiles_get_image(map_tiles_handle_t handle, int index) +{ + if (!handle || !handle->initialized || index < 0 || index >= handle->tile_count) { + return NULL; + } + + return &handle->tile_imgs[index]; +} + +uint8_t* map_tiles_get_buffer(map_tiles_handle_t handle, int index) +{ + if (!handle || !handle->initialized || index < 0 || index >= handle->tile_count) { + return NULL; + } + + return handle->tile_bufs[index]; +} + +void map_tiles_set_loading_error(map_tiles_handle_t handle, bool error) +{ + if (!handle || !handle->initialized) { + ESP_LOGE(TAG, "Handle not initialized"); + return; + } + + handle->tile_loading_error = error; +} + +bool map_tiles_has_loading_error(map_tiles_handle_t handle) +{ + if (!handle || !handle->initialized) { + return true; + } + + return handle->tile_loading_error; +} + +void map_tiles_cleanup(map_tiles_handle_t handle) +{ + if (!handle) { + return; + } + + if (handle->initialized) { + // Free tile buffers + if (handle->tile_bufs) { + for (int i = 0; i < handle->tile_count; i++) { + if (handle->tile_bufs[i]) { + heap_caps_free(handle->tile_bufs[i]); + handle->tile_bufs[i] = NULL; + } + } + free(handle->tile_bufs); + handle->tile_bufs = NULL; + } + + // Free tile image descriptors array + if (handle->tile_imgs) { + free(handle->tile_imgs); + handle->tile_imgs = NULL; + } + + handle->initialized = false; + ESP_LOGI(TAG, "Map tiles cleaned up"); + } + + // Free base path and folder names, then handle + if (handle->base_path) { + free(handle->base_path); + } + for (int i = 0; i < handle->tile_type_count; i++) { + if (handle->tile_folders[i]) { + free(handle->tile_folders[i]); + } + } + free(handle); +} + +void map_tiles_get_grid_size(map_tiles_handle_t handle, int* cols, int* rows) +{ + if (!handle || !handle->initialized || !cols || !rows) { + if (cols) *cols = 0; + if (rows) *rows = 0; + return; + } + + *cols = handle->grid_cols; + *rows = handle->grid_rows; +} + +int map_tiles_get_tile_count(map_tiles_handle_t handle) +{ + if (!handle || !handle->initialized) { + return 0; + } + + return handle->tile_count; +}